package analysis

import (
	"fmt"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"pgregory.net/rapid"

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

// Feature: multi-repo-support, Property 18: Cross-repo impact traversal
//
// For any graph containing Cross_Repo_Edges, AnalyzeImpact SHALL follow those
// edges during BFS traversal and include affected symbols from other repositories
// in the result. The result SHALL group affected symbols by RepoPrefix and set
// cross_repo_impact to true when symbols from multiple repos are affected.
func TestPropertyCrossRepoImpactTraversal(t *testing.T) {
	rapid.Check(t, func(rt *rapid.T) {
		g := graph.New()

		// Generate 2 repo prefixes.
		repoA := "repo-a"
		repoB := "repo-b"

		// Generate a random number of functions per repo (1-3).
		numFuncsA := rapid.IntRange(1, 3).Draw(rt, "numFuncsA")
		numFuncsB := rapid.IntRange(1, 3).Draw(rt, "numFuncsB")

		var repoAFuncs []string
		var repoBFuncs []string

		// Create repo-a functions.
		for i := 0; i < numFuncsA; i++ {
			id := fmt.Sprintf("%s/src/a.go::FuncA%d", repoA, i)
			g.AddNode(&graph.Node{
				ID:         id,
				Kind:       graph.KindFunction,
				Name:       fmt.Sprintf("FuncA%d", i),
				FilePath:   fmt.Sprintf("%s/src/a.go", repoA),
				Language:   "go",
				StartLine:  i*10 + 1,
				EndLine:    i*10 + 9,
				RepoPrefix: repoA,
			})
			repoAFuncs = append(repoAFuncs, id)
		}

		// Create repo-b functions.
		for i := 0; i < numFuncsB; i++ {
			id := fmt.Sprintf("%s/src/b.go::FuncB%d", repoB, i)
			g.AddNode(&graph.Node{
				ID:         id,
				Kind:       graph.KindFunction,
				Name:       fmt.Sprintf("FuncB%d", i),
				FilePath:   fmt.Sprintf("%s/src/b.go", repoB),
				Language:   "go",
				StartLine:  i*10 + 1,
				EndLine:    i*10 + 9,
				RepoPrefix: repoB,
			})
			repoBFuncs = append(repoBFuncs, id)
		}

		// Create a cross-repo edge: repo-b's first function calls repo-a's first function.
		g.AddEdge(&graph.Edge{
			From:      repoBFuncs[0],
			To:        repoAFuncs[0],
			Kind:      graph.EdgeCalls,
			CrossRepo: true,
		})

		// Optionally add intra-repo edges in repo-a.
		if numFuncsA > 1 {
			g.AddEdge(&graph.Edge{
				From: repoAFuncs[1],
				To:   repoAFuncs[0],
				Kind: graph.EdgeCalls,
			})
		}

		// Analyze impact starting from repo-a's first function.
		result := AnalyzeImpact(g, []string{repoAFuncs[0]}, nil, nil)
		require.NotNil(t, result)

		// The BFS should follow the cross-repo edge and find repo-b's function.
		foundCrossRepo := false
		for _, entries := range result.ByDepth {
			for _, entry := range entries {
				if entry.RepoPrefix == repoB {
					foundCrossRepo = true
				}
			}
		}
		if !foundCrossRepo {
			rt.Error("cross-repo edge was not followed: expected repo-b symbols in impact results")
		}

		// cross_repo_impact should be true since symbols from multiple repos are affected.
		if !result.CrossRepoImpact {
			rt.Error("CrossRepoImpact should be true when symbols from multiple repos are affected")
		}

		// ByRepo should group symbols by repo prefix.
		if result.ByRepo == nil {
			rt.Error("ByRepo should be populated when cross-repo impact is detected")
		}
		if len(result.ByRepo[repoB]) == 0 {
			rt.Error("ByRepo should contain repo-b entries")
		}
	})
}

// Feature: multi-repo-support, Property 19: Cross-repo safety checks
//
// For any signature change on a symbol that has callers in other repositories,
// VerifyChanges SHALL report violations for those cross-repo callers.
// For any changed symbol that has transitive test dependents in other repositories,
// get_test_targets SHALL include those cross-repo test files in the result.
func TestPropertyCrossRepoSafetyChecks(t *testing.T) {
	rapid.Check(t, func(rt *rapid.T) {
		g := graph.New()

		repoA := "repo-a"
		repoB := "repo-b"

		// Create a function in repo-a with a signature.
		targetID := fmt.Sprintf("%s/lib.go::DoWork", repoA)
		g.AddNode(&graph.Node{
			ID:         targetID,
			Kind:       graph.KindFunction,
			Name:       "DoWork",
			FilePath:   fmt.Sprintf("%s/lib.go", repoA),
			Language:   "go",
			StartLine:  1,
			EndLine:    10,
			RepoPrefix: repoA,
			Meta:       map[string]any{"signature": "func DoWork(x int, y string)"},
		})

		// Create a caller in repo-b (cross-repo).
		callerID := fmt.Sprintf("%s/main.go::UsesDoWork", repoB)
		g.AddNode(&graph.Node{
			ID:         callerID,
			Kind:       graph.KindFunction,
			Name:       "UsesDoWork",
			FilePath:   fmt.Sprintf("%s/main.go", repoB),
			Language:   "go",
			StartLine:  5,
			EndLine:    15,
			RepoPrefix: repoB,
		})

		// Cross-repo call edge.
		g.AddEdge(&graph.Edge{
			From:      callerID,
			To:        targetID,
			Kind:      graph.EdgeCalls,
			CrossRepo: true,
		})

		// Create a test file in repo-b that depends on the caller.
		testID := fmt.Sprintf("%s/main_test.go::TestUsesDoWork", repoB)
		g.AddNode(&graph.Node{
			ID:         testID,
			Kind:       graph.KindFunction,
			Name:       "TestUsesDoWork",
			FilePath:   fmt.Sprintf("%s/main_test.go", repoB),
			Language:   "go",
			StartLine:  1,
			EndLine:    10,
			RepoPrefix: repoB,
		})
		g.AddEdge(&graph.Edge{
			From: testID,
			To:   callerID,
			Kind: graph.EdgeCalls,
		})

		// Generate a random parameter count change.
		newParamCount := rapid.IntRange(0, 1).Draw(rt, "newParamCount")
		var newSig string
		if newParamCount == 0 {
			newSig = "func DoWork()"
		} else {
			newSig = "func DoWork(x int)"
		}

		// Verify changes: the cross-repo caller should be reported as a violation.
		engine := query.NewEngine(g)
		result := VerifyChanges(g, engine, []SignatureChange{
			{SymbolID: targetID, NewSignature: newSig},
		})

		require.NotNil(t, result)

		// Should have violations from the cross-repo caller.
		foundCrossRepoCaller := false
		for _, v := range result.Violations {
			if v.RepoPrefix == repoB {
				foundCrossRepoCaller = true
			}
		}
		if !foundCrossRepoCaller {
			rt.Error("VerifyChanges should report violations for cross-repo callers")
		}

		// CrossRepoViolations flag should be set.
		if !result.CrossRepoViolations {
			rt.Error("CrossRepoViolations should be true when violations come from other repos")
		}

		// Test targets: AnalyzeImpact should include cross-repo test files.
		impact := AnalyzeImpact(g, []string{targetID}, nil, nil)
		require.NotNil(t, impact)

		foundTestFile := false
		for _, tf := range impact.TestFiles {
			if tf == fmt.Sprintf("%s/main_test.go", repoB) {
				foundTestFile = true
			}
		}
		if !foundTestFile {
			rt.Error("get_test_targets should include cross-repo test files")
		}
	})
}

// TestCrossRepoImpact_Unit is a focused unit test for cross-repo impact analysis.
func TestCrossRepoImpact_Unit(t *testing.T) {
	g := graph.New()

	// Repo A: library function
	g.AddNode(&graph.Node{ID: "repo-a/lib.go::Helper", Kind: graph.KindFunction, Name: "Helper",
		FilePath: "repo-a/lib.go", Language: "go", StartLine: 1, EndLine: 10, RepoPrefix: "repo-a"})

	// Repo B: consumer
	g.AddNode(&graph.Node{ID: "repo-b/app.go::UseHelper", Kind: graph.KindFunction, Name: "UseHelper",
		FilePath: "repo-b/app.go", Language: "go", StartLine: 1, EndLine: 10, RepoPrefix: "repo-b"})

	// Cross-repo edge
	g.AddEdge(&graph.Edge{From: "repo-b/app.go::UseHelper", To: "repo-a/lib.go::Helper", Kind: graph.EdgeCalls, CrossRepo: true})

	result := AnalyzeImpact(g, []string{"repo-a/lib.go::Helper"}, nil, nil)

	require.NotNil(t, result)
	assert.True(t, result.CrossRepoImpact)
	assert.Contains(t, result.ByRepo, "repo-b")
	assert.Len(t, result.ByRepo["repo-b"], 1)
	assert.Equal(t, "repo-b/app.go::UseHelper", result.ByRepo["repo-b"][0].ID)
}
