package mcp

import (
	"context"
	"encoding/json"
	"os"
	"path/filepath"
	"strings"
	"testing"

	mcplib "github.com/mark3labs/mcp-go/mcp"
	"github.com/stretchr/testify/assert"
	"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/indexer"
	"github.com/zzet/gortex/internal/parser"
	"github.com/zzet/gortex/internal/parser/languages"
	"github.com/zzet/gortex/internal/query"
	"github.com/zzet/gortex/internal/search"
)

// setupRepoWithHTTPProvider creates a tiny Go repo with Gin route declarations.
// Produces http provider contracts on index.
func setupRepoWithHTTPProvider(t *testing.T, root, name string) string {
	t.Helper()
	dir := filepath.Join(root, name)
	require.NoError(t, os.MkdirAll(dir, 0o755))
	require.NoError(t, os.WriteFile(
		filepath.Join(dir, "main.go"),
		[]byte(`package main

import "github.com/gin-gonic/gin"

func setupRoutes(r *gin.Engine) {
	r.GET("/api/users", listUsers)
	r.POST("/api/users", createUser)
}

func listUsers()  {}
func createUser() {}
`),
		0o644,
	))
	return dir
}

// setupRepoWithHTTPConsumer creates a tiny Go repo with http.Get calls.
// Produces http consumer contracts on index.
func setupRepoWithHTTPConsumer(t *testing.T, root, name string) string {
	t.Helper()
	dir := filepath.Join(root, name)
	require.NoError(t, os.MkdirAll(dir, 0o755))
	require.NoError(t, os.WriteFile(
		filepath.Join(dir, "client.go"),
		[]byte(`package main

import "net/http"

func fetchUsers() {
	http.Get("http://api.example.com/api/users")
}
`),
		0o644,
	))
	return dir
}

// TestHandleContracts_ReflectsRuntimeTrackedRepos is the regression guard for
// the T0.1 bug: the contracts tool previously held a snapshot Registry that
// was bound once at startup. Repos tracked at runtime (via track_repository)
// added contracts to per-repo indexers but were invisible to contracts
// list/check. This test fails unless the handler pulls a fresh merged
// registry on every call.
func TestHandleContracts_ReflectsRuntimeTrackedRepos(t *testing.T) {
	root := t.TempDir()
	providerRepo := setupRepoWithHTTPProvider(t, root, "provider-svc")

	tmpCfg := filepath.Join(t.TempDir(), "config.yaml")
	gc := &config.GlobalConfig{
		Repos: []config.RepoEntry{
			{Path: providerRepo, Name: "provider-svc"},
		},
	}
	gc.SetConfigPath(tmpCfg)
	require.NoError(t, gc.Save())

	cm, err := config.NewConfigManager(tmpCfg)
	require.NoError(t, err)

	preg := parser.NewRegistry()
	languages.RegisterAll(preg)

	g := graph.New()
	mi := indexer.NewMultiIndexer(g, preg, search.NewBM25(), cm, zap.NewNop())
	_, err = mi.IndexAll()
	require.NoError(t, err)

	eng := query.NewEngine(g)
	singleton := indexer.New(g, preg, config.IndexConfig{}, zap.NewNop())
	srv := NewServer(eng, g, singleton, nil, zap.NewNop(), nil, MultiRepoOptions{
		ConfigManager: cm,
		MultiIndexer:  mi,
	})

	// Baseline: contracts from the provider repo are visible.
	providerCount := contractsTotal(t, srv, nil)
	require.Greater(t, providerCount, 0,
		"expected contracts from provider-svc to be visible before runtime track")

	// Track a new repo at runtime. This is the scenario that used to silently
	// fail: the indexer learned about it, but the contracts tool kept serving
	// the pre-track snapshot.
	consumerRepo := setupRepoWithHTTPConsumer(t, root, "consumer-svc")
	trackReq := mcplib.CallToolRequest{}
	trackReq.Params.Arguments = map[string]any{
		"path": consumerRepo,
		"name": "consumer-svc",
	}
	trackRes, err := srv.handleTrackRepository(context.Background(), trackReq)
	require.NoError(t, err)
	require.NotNil(t, trackRes)
	assert.False(t, trackRes.IsError,
		"track_repository should succeed; got %+v", trackRes.Content)

	// Post-track: total contracts must increase and the consumer repo must
	// contribute at least one contract of its own.
	totalAfter := contractsTotal(t, srv, nil)
	assert.Greater(t, totalAfter, providerCount,
		"contracts total should grow after runtime track; before=%d after=%d",
		providerCount, totalAfter)

	consumerCount := contractsTotal(t, srv, map[string]any{"repo": "consumer-svc"})
	assert.Greater(t, consumerCount, 0,
		"consumer-svc contracts must be visible via repo filter after runtime track")

	providerCountAfter := contractsTotal(t, srv, map[string]any{"repo": "provider-svc"})
	assert.Equal(t, providerCount, providerCountAfter,
		"tracking a new repo must not drop provider-svc contracts")
}

// TestHandleContracts_MatchesGraphContractCount guards against the symptom the
// tuck audit hit directly: graph holds N contract nodes while contracts list
// returns 0. After the fix, the counts must agree within the same registry
// snapshot.
func TestHandleContracts_MatchesGraphContractCount(t *testing.T) {
	root := t.TempDir()
	repo := setupRepoWithHTTPProvider(t, root, "svc-a")

	tmpCfg := filepath.Join(t.TempDir(), "config.yaml")
	gc := &config.GlobalConfig{
		Repos: []config.RepoEntry{{Path: repo, Name: "svc-a"}},
	}
	gc.SetConfigPath(tmpCfg)
	require.NoError(t, gc.Save())

	cm, err := config.NewConfigManager(tmpCfg)
	require.NoError(t, err)

	preg := parser.NewRegistry()
	languages.RegisterAll(preg)

	g := graph.New()
	mi := indexer.NewMultiIndexer(g, preg, search.NewBM25(), cm, zap.NewNop())
	_, err = mi.IndexAll()
	require.NoError(t, err)

	eng := query.NewEngine(g)
	srv := NewServer(eng, g, nil, nil, zap.NewNop(), nil, MultiRepoOptions{
		ConfigManager: cm,
		MultiIndexer:  mi,
	})

	graphContractNodes := 0
	for _, n := range g.AllNodes() {
		if n.Kind == graph.KindContract {
			graphContractNodes++
		}
	}
	require.Greater(t, graphContractNodes, 0, "provider repo should produce contract nodes in graph")

	toolTotal := contractsTotal(t, srv, nil)
	assert.Equal(t, graphContractNodes, toolTotal,
		"contracts list must surface every contract-kind node in the graph")
}

// contractsTotal calls handleContracts (action defaults to list) and extracts
// the integer "total" from the JSON payload. Fails the test on any error or
// malformed response.
func contractsTotal(t *testing.T, srv *Server, args map[string]any) int {
	t.Helper()
	req := mcplib.CallToolRequest{}
	if args == nil {
		req.Params.Arguments = map[string]any{}
	} else {
		req.Params.Arguments = args
	}
	res, err := srv.handleContracts(context.Background(), req)
	require.NoError(t, err)
	require.NotNil(t, res)
	require.False(t, res.IsError, "contracts list returned error: %+v", res.Content)

	payload := extractTextFromContent(t, res.Content)
	var parsed struct {
		Total int `json:"total"`
	}
	require.NoError(t, json.Unmarshal([]byte(payload), &parsed),
		"contracts list payload is not JSON: %s", payload)
	return parsed.Total
}

// extractTextFromContent pulls the serialized text body out of an MCP tool
// result. NewToolResultJSON wraps the JSON in a single TextContent block.
func extractTextFromContent(t *testing.T, content []mcplib.Content) string {
	t.Helper()
	require.NotEmpty(t, content, "tool result has no content blocks")
	var b strings.Builder
	for _, c := range content {
		if tc, ok := c.(mcplib.TextContent); ok {
			b.WriteString(tc.Text)
		}
	}
	out := b.String()
	require.NotEmpty(t, out, "no text content in tool result")
	return out
}
