package mcp

import (
	"context"
	"strings"
	"testing"

	"github.com/mark3labs/mcp-go/mcp"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// TestDefaultFormatForClient verifies the GCX-capable client allowlist.
// The list is the contract: clients in it get gcx by default; everything
// else falls back to JSON. Drift in this list silently changes the wire
// format every shipping client receives, so the test pins it explicitly.
func TestDefaultFormatForClient(t *testing.T) {
	cases := []struct {
		client string
		want   string
	}{
		// GCX-capable: every client whose plugin/CLI ships a GCX1 decoder.
		{"claude-code", "gcx"},
		{"Claude-Code", "gcx"},     // case-insensitive
		{"  claude-code  ", "gcx"}, // trimmed
		{"cursor", "gcx"},
		{"vscode", "gcx"},
		{"zed", "gcx"},
		{"aider", "gcx"},
		{"kilocode", "gcx"},
		{"opencode", "gcx"},
		{"openclaw", "gcx"},
		{"codex", "gcx"},

		// Unknown / unset → JSON fallback.
		{"", ""},
		{"some-other-client", ""},
		{"unknown", ""},
	}
	for _, tc := range cases {
		assert.Equal(t, tc.want, defaultFormatForClient(tc.client),
			"defaultFormatForClient(%q)", tc.client)
	}
}

// TestResolveSessionFormat_NoSession returns "" for a bare context — no
// session means no client identity, which means no default format.
func TestResolveSessionFormat_NoSession(t *testing.T) {
	srv, _ := setupTestServer(t)
	assert.Equal(t, "", srv.resolveSessionFormat(context.Background()))
}

// TestResolveSessionFormat_KnownClient verifies the full per-session
// path: NoteSessionClient stores the client name, and resolveSessionFormat
// reads it back through sessionFor + defaultFormatForClient.
func TestResolveSessionFormat_KnownClient(t *testing.T) {
	srv, _ := setupTestServer(t)
	srv.NoteSessionClient("session_X", "claude-code", "1.0.42")
	ctx := WithSessionID(context.Background(), "session_X")
	assert.Equal(t, "gcx", srv.resolveSessionFormat(ctx))
}

func TestResolveSessionFormat_UnknownClient(t *testing.T) {
	srv, _ := setupTestServer(t)
	srv.NoteSessionClient("session_X", "some-bespoke-client", "0.1")
	ctx := WithSessionID(context.Background(), "session_X")
	assert.Equal(t, "", srv.resolveSessionFormat(ctx),
		"unknown client must fall back to JSON (empty string)")
}

// TestNoteSessionClient_NilSafe ensures NoteSessionClient never panics
// when called on a nil *Server or with empty inputs — both are normal
// during boot races / embedded-mode tests.
func TestNoteSessionClient_NilSafe(t *testing.T) {
	var srv *Server
	srv.NoteSessionClient("sess", "claude-code", "1.0")

	srv2, _ := setupTestServer(t)
	srv2.NoteSessionClient("", "claude-code", "1.0") // empty session id → no-op
	srv2.NoteSessionClient("sess", "", "1.0")        // empty client → no-op
}

// TestNoteSessionClient_IsolatedPerSession verifies two sessions get
// independent client-name state. This is the core invariant that lets
// the daemon serve multiple proxies through one shared *Server.
func TestNoteSessionClient_IsolatedPerSession(t *testing.T) {
	srv, _ := setupTestServer(t)

	srv.NoteSessionClient("sess_A", "claude-code", "1.0")
	srv.NoteSessionClient("sess_B", "some-bespoke-client", "0.1")

	ctxA := WithSessionID(context.Background(), "sess_A")
	ctxB := WithSessionID(context.Background(), "sess_B")
	assert.Equal(t, "gcx", srv.resolveSessionFormat(ctxA))
	assert.Equal(t, "", srv.resolveSessionFormat(ctxB))
}

// TestIsGCX_ExplicitFormatWins verifies that an explicit `format` arg
// overrides the per-session default in either direction.
func TestIsGCX_ExplicitFormatWins(t *testing.T) {
	srv, _ := setupTestServer(t)
	srv.NoteSessionClient("sess_A", "claude-code", "1.0") // session default = gcx
	ctx := WithSessionID(context.Background(), "sess_A")

	// Explicit "json" must override the session default.
	req := mcp.CallToolRequest{}
	req.Params.Arguments = map[string]any{"format": "json"}
	assert.False(t, srv.isGCX(ctx, req),
		"explicit format=json must defeat session-default gcx")

	// Explicit "gcx" stays gcx.
	req2 := mcp.CallToolRequest{}
	req2.Params.Arguments = map[string]any{"format": "gcx"}
	assert.True(t, srv.isGCX(ctx, req2))
}

// TestIsGCX_SessionDefaultApplies verifies that a session whose client
// is GCX-capable picks gcx when the request omits `format`.
func TestIsGCX_SessionDefaultApplies(t *testing.T) {
	srv, _ := setupTestServer(t)
	srv.NoteSessionClient("sess_A", "claude-code", "1.0")
	ctx := WithSessionID(context.Background(), "sess_A")

	req := mcp.CallToolRequest{}
	req.Params.Arguments = map[string]any{} // no format
	assert.True(t, srv.isGCX(ctx, req),
		"claude-code session with no explicit format must default to gcx")
}

// TestIsGCX_NoSession_NoFormat returns false — the legacy default is
// JSON, and absent both an explicit format and a known client we must
// preserve that.
func TestIsGCX_NoSession_NoFormat(t *testing.T) {
	srv, _ := setupTestServer(t)
	req := mcp.CallToolRequest{}
	req.Params.Arguments = map[string]any{}
	assert.False(t, srv.isGCX(context.Background(), req))
}

// TestIsTOON_ExplicitFormatWins verifies that an explicit `format=toon`
// trips isTOON regardless of session default.
func TestIsTOON_ExplicitFormatWins(t *testing.T) {
	srv, _ := setupTestServer(t)
	srv.NoteSessionClient("sess_A", "claude-code", "1.0") // gcx by default
	ctx := WithSessionID(context.Background(), "sess_A")

	req := mcp.CallToolRequest{}
	req.Params.Arguments = map[string]any{"format": "toon"}
	assert.True(t, srv.isTOON(ctx, req))
	assert.False(t, srv.isGCX(ctx, req),
		"format=toon must not also trigger gcx")
}

// TestRespondJSONOrTOON_RoutesByFormat pins the helper that 14
// list-shaped tools share. With explicit format=toon the payload comes
// back as TOON-marshalled text; with no format and an unknown client
// it falls back to JSON. This is the single decision point most tools
// route through, so a regression here would silently flip every
// downstream consumer to the wrong format.
func TestRespondJSONOrTOON_RoutesByFormat(t *testing.T) {
	srv, _ := setupTestServer(t)
	payload := map[string]any{"x": 1, "y": "two"}

	// format=toon → TOON text result.
	reqTOON := mcp.CallToolRequest{}
	reqTOON.Params.Arguments = map[string]any{"format": "toon"}
	res, err := srv.respondJSONOrTOON(context.Background(), reqTOON, payload)
	assert.NoError(t, err)
	assert.False(t, res.IsError)
	tc, ok := res.Content[0].(mcp.TextContent)
	assert.True(t, ok, "expected TextContent for TOON result")
	// TOON encodes scalar map values with `key: value` lines; an empty
	// or JSON-shaped payload would not contain that exact prefix.
	assert.Contains(t, tc.Text, "x: 1")
	assert.NotContains(t, tc.Text, "{")

	// no format, unknown client → JSON fallback.
	reqJSON := mcp.CallToolRequest{}
	reqJSON.Params.Arguments = map[string]any{}
	res, err = srv.respondJSONOrTOON(context.Background(), reqJSON, payload)
	assert.NoError(t, err)
	tc, ok = res.Content[0].(mcp.TextContent)
	assert.True(t, ok)
	assert.Contains(t, tc.Text, "{") // JSON object braces

	// format=json overrides session default → JSON.
	srv.NoteSessionClient("sess_T", "claude-code", "1.0") // session default would be gcx, not toon
	ctx := WithSessionID(context.Background(), "sess_T")
	reqExplicitJSON := mcp.CallToolRequest{}
	reqExplicitJSON.Params.Arguments = map[string]any{"format": "json"}
	res, err = srv.respondJSONOrTOON(ctx, reqExplicitJSON, payload)
	assert.NoError(t, err)
	tc, ok = res.Content[0].(mcp.TextContent)
	assert.True(t, ok)
	assert.Contains(t, tc.Text, "{")
}

// TestRespondJSONOrTOON_MaxTokensTrimAndDecorate is the end-to-end
// pin for the new max_tokens parameter: a payload that exceeds the
// derived byte cap must come back trimmed AND carry the
// _truncated_by_tokens marker so the agent can see the tokens
// budget was the constraint that fired. Pairs with the unit tests
// in budget_test.go — this one rides the actual respondJSONOrTOON
// path that get_file_summary / find_usages / get_call_chain /
// get_editing_context / prefetch_context all share.
func TestRespondJSONOrTOON_MaxTokensTrimAndDecorate(t *testing.T) {
	srv, _ := setupTestServer(t)

	// Build a payload large enough that a tight max_tokens cap must
	// trim. 200 rows × ~80 bytes per row ≈ 16 KB raw payload.
	rows := make([]any, 200)
	for i := range rows {
		rows[i] = map[string]any{
			"id":   "row-" + strings.Repeat("x", 40),
			"line": i,
			"meta": strings.Repeat("padding-", 10),
		}
	}
	payload := map[string]any{
		"results": rows,
		"total":   200,
	}

	// max_tokens=200 ≈ 700 bytes — clearly too small for the raw
	// payload, so trim should fire.
	req := mcp.CallToolRequest{}
	req.Params.Arguments = map[string]any{
		"format":     "json",
		"max_tokens": float64(200),
	}
	res, err := srv.respondJSONOrTOON(context.Background(), req, payload)
	require.NoError(t, err)
	require.False(t, res.IsError)

	tc, ok := res.Content[0].(mcp.TextContent)
	require.True(t, ok)

	// The response must carry both the generic truncation flag AND
	// the tokens-specific marker, plus the original max_tokens value
	// for caller diagnostics.
	assert.Contains(t, tc.Text, "_truncated_by_budget")
	assert.Contains(t, tc.Text, "_truncated_by_tokens")
	assert.Contains(t, tc.Text, "_max_tokens")

	// Same payload with a generous max_tokens cap → no trim, no markers.
	req.Params.Arguments = map[string]any{
		"format":     "json",
		"max_tokens": float64(100000),
	}
	res, err = srv.respondJSONOrTOON(context.Background(), req, payload)
	require.NoError(t, err)
	tc, ok = res.Content[0].(mcp.TextContent)
	require.True(t, ok)
	assert.NotContains(t, tc.Text, "_truncated_by_budget")
	assert.NotContains(t, tc.Text, "_truncated_by_tokens")
}

// TestGCXResponseWithBudget_MaxTokensTrimAndDecorate covers the GCX
// path's max_tokens behaviour: a GCX payload exceeding the
// token-derived byte cap gets row-trimmed by trimGCXBytes and then
// stamped with a `# max_tokens=N truncated_by_tokens=true` comment by
// decorateTokenBudgetGCX. The header stays intact so decoders still
// see a valid GCX1 envelope.
func TestGCXResponseWithBudget_MaxTokensTrimAndDecorate(t *testing.T) {
	srv, _ := setupTestServer(t)

	// Build a synthetic GCX payload that is comfortably over a
	// 200-token (~700 byte) cap.
	var sb strings.Builder
	sb.WriteString("GCX1 tool=test fields=id,kind,name\n")
	for i := 0; i < 50; i++ {
		sb.WriteString("internal/foo.go::Bar")
		sb.WriteString(strings.Repeat("x", 20))
		sb.WriteString("\tfunction\tBar\n")
	}
	payload := []byte(sb.String())

	req := mcp.CallToolRequest{}
	req.Params.Arguments = map[string]any{
		"format":     "gcx",
		"max_tokens": float64(200),
	}
	res, err := srv.gcxResponseWithBudget(req)(payload, nil)
	require.NoError(t, err)
	require.False(t, res.IsError)

	tc, ok := res.Content[0].(mcp.TextContent)
	require.True(t, ok)
	assert.True(t, strings.HasPrefix(tc.Text, "GCX1 tool=test"))
	// Both the byte-level and token-level markers must appear.
	assert.Contains(t, tc.Text, "# truncated_by_budget=true")
	assert.Contains(t, tc.Text, "max_tokens=200")
	assert.Contains(t, tc.Text, "truncated_by_tokens=true")
}
