package mcp

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

	mcplib "github.com/mark3labs/mcp-go/mcp"
	"github.com/stretchr/testify/require"
	"go.uber.org/zap"

	"github.com/zzet/gortex/internal/config"
	"github.com/zzet/gortex/internal/graph"
	"github.com/zzet/gortex/internal/query"
	"github.com/zzet/gortex/internal/search"
)

// equivalenceTestServer builds a single-repo server with no LLM
// provider, so any expansion observed comes purely from the
// deterministic equivalence channel.
func equivalenceTestServer(t *testing.T, names []string, equivEnabled *bool) *Server {
	t.Helper()
	g := graph.New()
	bm := search.NewBM25()
	for i, n := range names {
		id := "pkg/" + n + ".go::" + n
		g.AddNode(&graph.Node{
			ID: id, Kind: graph.KindFunction, Name: n,
			FilePath: "pkg/" + n + ".go", StartLine: i + 1, EndLine: i + 5, Language: "go",
		})
		bm.Add(id, n, "pkg/"+n+".go", "")
	}
	eng := query.NewEngine(g)
	eng.SetSearch(bm)
	srv := NewServer(eng, g, nil, nil, zap.NewNop(), nil)
	srv.SetSearchConfig(config.SearchConfig{EquivalenceClasses: equivEnabled})
	srv.RunAnalysis() // builds the auto-concept vocabulary
	return srv
}

func searchIDs(t *testing.T, srv *Server, args map[string]any) map[string]bool {
	t.Helper()
	req := mcplib.CallToolRequest{}
	req.Params.Name = "search_symbols"
	req.Params.Arguments = args
	res, err := srv.handleSearchSymbols(context.Background(), req)
	require.NoError(t, err)
	require.Falsef(t, res.IsError, "search errored: %v", res.Content)
	var resp map[string]any
	require.NoError(t, json.Unmarshal([]byte(res.Content[0].(mcplib.TextContent).Text), &resp))
	ids := map[string]bool{}
	if results, ok := resp["results"].([]any); ok {
		for _, r := range results {
			ids[r.(map[string]any)["id"].(string)] = true
		}
	}
	return ids
}

// TestSearchSymbols_EquivalenceBridgesVocabulary is the core feature
// test: a query for "auth" must reach a LoginService symbol via the
// curated equivalence class -- with NO LLM provider configured.
func TestSearchSymbols_EquivalenceBridgesVocabulary(t *testing.T) {
	srv := equivalenceTestServer(t, []string{
		"LoginService", "SigninController", "ParseConfig", "UnrelatedThing",
	}, nil) // nil EquivalenceClasses == enabled by default

	ids := searchIDs(t, srv, map[string]any{"query": "auth"})
	require.Truef(t, ids["pkg/LoginService.go::LoginService"],
		"equivalence expansion should bridge 'auth' to LoginService; got %v", ids)
	require.Truef(t, ids["pkg/SigninController.go::SigninController"],
		"equivalence expansion should bridge 'auth' to SigninController; got %v", ids)
}

// TestSearchSymbols_EquivalenceDeleteRemove confirms the delete/remove
// bridge works in both directions through the BM25 OR-merge.
func TestSearchSymbols_EquivalenceDeleteRemove(t *testing.T) {
	srv := equivalenceTestServer(t, []string{
		"RemoveUser", "DropTable", "FetchProfile",
	}, nil)
	ids := searchIDs(t, srv, map[string]any{"query": "delete"})
	require.Truef(t, ids["pkg/RemoveUser.go::RemoveUser"],
		"'delete' should bridge to RemoveUser; got %v", ids)
	require.Truef(t, ids["pkg/DropTable.go::DropTable"],
		"'delete' should bridge to DropTable; got %v", ids)
}

// TestSearchSymbols_ExpandOffDisablesEquivalence confirms expand:"off"
// suppresses the equivalence channel -- "auth" then matches nothing.
func TestSearchSymbols_ExpandOffDisablesEquivalence(t *testing.T) {
	srv := equivalenceTestServer(t, []string{"LoginService", "SigninController"}, nil)
	ids := searchIDs(t, srv, map[string]any{"query": "auth", "expand": "off"})
	require.Falsef(t, ids["pkg/LoginService.go::LoginService"],
		"expand:off must not bridge 'auth' to LoginService; got %v", ids)
}

// TestSearchSymbols_EquivalenceDisabledViaConfig confirms the config
// toggle (equivalence_classes:false) disables the channel.
func TestSearchSymbols_EquivalenceDisabledViaConfig(t *testing.T) {
	off := false
	srv := equivalenceTestServer(t, []string{"LoginService"}, &off)
	ids := searchIDs(t, srv, map[string]any{"query": "auth"})
	require.Falsef(t, ids["pkg/LoginService.go::LoginService"],
		"equivalence_classes:false must disable the channel; got %v", ids)
}

// TestSearchSymbols_EquivalenceAutoConcepts confirms the per-repo
// auto-mined concept vocabulary bridges a domain phrase: a query for
// "blast" reaches a symbol named only with "radius" because the two
// words co-occur across the repo's symbol names.
func TestSearchSymbols_EquivalenceAutoConcepts(t *testing.T) {
	srv := equivalenceTestServer(t, []string{
		"handleBlastRadius", "blastRadiusOf", "BlastRadiusReport",
		"computeBlastRadius", "radiusOnlySymbol",
	}, nil)
	ids := searchIDs(t, srv, map[string]any{"query": "blast"})
	require.Truef(t, ids["pkg/radiusOnlySymbol.go::radiusOnlySymbol"],
		"auto-concept mining should bridge 'blast' to a radius-named symbol; got %v", ids)
}
