package headless

// Tests for the sub-agent RPC subprocess mode (US-019, #135): the pure
// filterBuiltinTools helper and the RunSubAgentRPC validation branches
// (method-not-found, invalid params, parse error) that return RPC errors before
// any provider is resolved. The happy-path transport is covered in
// internal/runtime via a compiled helper binary; RunSubAgentOnce (the agent
// core) is covered there with a faux provider.

import (
	"bytes"
	"context"
	"encoding/json"
	"strings"
	"testing"

	"github.com/smallnest/pigo/internal/agentcore"
	"github.com/smallnest/pigo/internal/cli/run"
	"github.com/smallnest/pigo/internal/jsonrpc"
	"github.com/smallnest/pigo/internal/runtime"
)

// TestFilterBuiltinTools verifies the subprocess tool filter: an empty name
// list keeps all builtins, a subset keeps only the named tools, and unknown
// names are silently ignored.
func TestFilterBuiltinTools(t *testing.T) {
	all := run.BuiltinTools(t.TempDir(), false)
	if len(all) == 0 {
		t.Fatal("BuiltinTools returned no tools")
	}
	namesOf := func(ts []agentcore.AgentTool) []string {
		out := make([]string, len(ts))
		for i, tl := range ts {
			out[i] = tl.Name()
		}
		return out
	}

	if got := filterBuiltinTools(all, nil); len(got) != len(all) {
		t.Errorf("nil names kept %d, want all %d", len(got), len(all))
	}
	if got := filterBuiltinTools(all, []string{}); len(got) != len(all) {
		t.Errorf("empty names kept %d, want all %d", len(got), len(all))
	}

	got := filterBuiltinTools(all, []string{"read", "grep"})
	if len(got) != 2 {
		t.Fatalf("subset kept %d, want 2: %v", len(got), namesOf(got))
	}
	gotNames := namesOf(got)
	if gotNames[0] != "read" || gotNames[1] != "grep" {
		t.Errorf("subset names = %v, want [read grep]", gotNames)
	}

	// Unknown names are ignored, known ones kept.
	got = filterBuiltinTools(all, []string{"read", "does-not-exist"})
	if len(got) != 1 || got[0].Name() != "read" {
		t.Errorf("unknown-name filter kept %v, want [read]", namesOf(got))
	}
}

// TestRunSubAgentRPCValidation verifies the subprocess returns JSON-RPC errors
// for malformed requests without reaching provider resolution: a parse error,
// an unknown method, and missing prompt/model params. These branches are the
// server's contract for bad input and need not involve a provider.
func TestRunSubAgentRPCValidation(t *testing.T) {
	cases := []struct {
		name     string
		line     string
		wantCode int
		wantMsg  string
	}{
		{"parse error", "not-json", -32700, "parse error"},
		{"method not found", `{"jsonrpc":"2.0","id":1,"method":"other","params":{}}`, -32601, "method not found"},
		{"missing prompt", `{"jsonrpc":"2.0","id":2,"method":"` + runtime.SubAgentRPCMethod + `","params":{"model":"x"}}`, -32602, "prompt and model are required"},
		{"missing model", `{"jsonrpc":"2.0","id":3,"method":"` + runtime.SubAgentRPCMethod + `","params":{"prompt":"x"}}`, -32602, "prompt and model are required"},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			var out, errOut bytes.Buffer
			code := RunSubAgentRPC(context.Background(), strings.NewReader(c.line+"\n"), &out, &errOut)
			if code != 0 {
				t.Errorf("exit code = %d, want 0 (validation errors are RPC responses, not non-zero exits)", code)
			}
			line, _ := out.ReadString('\n')
			if line == "" {
				t.Fatal("no response written")
			}
			var resp jsonrpc.Response
			if err := json.Unmarshal([]byte(line), &resp); err != nil {
				t.Fatalf("unmarshal response %q: %v", line, err)
			}
			if resp.Error == nil {
				t.Fatalf("response has no error: %s", line)
			}
			if resp.Error.Code != c.wantCode {
				t.Errorf("error code = %d, want %d (msg=%q)", resp.Error.Code, c.wantCode, resp.Error.Message)
			}
			if !strings.Contains(resp.Error.Message, c.wantMsg) {
				t.Errorf("error msg = %q, want it to contain %q", resp.Error.Message, c.wantMsg)
			}
		})
	}
}

// TestRunSubAgentRPCScannerError verifies a stdin read error (a line exceeding
// the scanner cap) is surfaced on stderr and yields a non-zero exit, rather
// than a silent clean exit.
func TestRunSubAgentRPCScannerError(t *testing.T) {
	// A line longer than the 16 MiB scanner cap triggers a scanner error.
	huge := strings.Repeat("a", 17*1024*1024)
	var out, errOut bytes.Buffer
	code := RunSubAgentRPC(context.Background(), strings.NewReader(huge), &out, &errOut)
	if code == 0 {
		t.Error("exit code = 0 on scanner error, want non-zero")
	}
	if !strings.Contains(errOut.String(), "subagent-rpc stdin") {
		t.Errorf("stderr = %q, want it to mention 'subagent-rpc stdin'", errOut.String())
	}
}
