package analysis

import (
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/zzet/gortex/internal/graph"
	"github.com/zzet/gortex/internal/query"
	"pgregory.net/rapid"
)

// --- Mock SourceReader ---

type mockSourceReader struct {
	g        *graph.Graph
	rootPath string
}

func (m *mockSourceReader) Graph() *graph.Graph { return m.g }
func (m *mockSourceReader) ResolveFilePath(relPath string) string {
	if filepath.IsAbs(relPath) {
		return relPath
	}
	if m.rootPath == "" {
		return ""
	}
	return filepath.Join(m.rootPath, relPath)
}

// --- Helpers ---

// writeGoFile creates a Go source file in tmpDir at the given relative path
// with the provided content. Returns the relative path.
func writeGoFile(t *testing.T, tmpDir, relPath, content string) {
	t.Helper()
	absPath := filepath.Join(tmpDir, relPath)
	require.NoError(t, os.MkdirAll(filepath.Dir(absPath), 0o755))
	require.NoError(t, os.WriteFile(absPath, []byte(content), 0o644))
}

// Feature: gortex-enhancements, Property 7: Scaffold name substitution

// --- Generators ---

// genValidGoIdent generates a valid Go identifier (uppercase letter + lowercase suffix).
func genValidGoIdent() *rapid.Generator[string] {
	return rapid.Custom(func(t *rapid.T) string {
		upper := []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J",
			"K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"}
		lower := []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j",
			"k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"}

		first := rapid.SampledFrom(upper).Draw(t, "first")
		suffixLen := rapid.IntRange(2, 8).Draw(t, "suffixLen")
		var sb strings.Builder
		sb.WriteString(first)
		for i := range suffixLen {
			sb.WriteString(rapid.SampledFrom(lower).Draw(t, fmt.Sprintf("s%d", i)))
		}
		return sb.String()
	})
}

// genDistinctIdentPair generates two distinct Go identifiers.
func genDistinctIdentPair() *rapid.Generator[[2]string] {
	return rapid.Custom(func(t *rapid.T) [2]string {
		a := genValidGoIdent().Draw(t, "identA")
		b := genValidGoIdent().Draw(t, "identB")
		// Ensure they are distinct
		for b == a {
			b = genValidGoIdent().Draw(t, "identBRetry")
		}
		return [2]string{a, b}
	})
}

// --- Property Tests ---

// TestPropertyScaffoldNameSubstitution verifies that for any existing symbol
// used as an example and for any valid new name, the scaffold output contains
// the new name in place of the example's name in all generated code, and each
// ScaffoldEdit has non-empty file_path, a positive insertion_line, and non-empty code.
func TestPropertyScaffoldNameSubstitution(t *testing.T) {
	rapid.Check(t, func(rt *rapid.T) {
		names := genDistinctIdentPair().Draw(rt, "names")
		exampleName := names[0]
		newName := names[1]

		// Create a temp directory with a Go source file containing the example function
		tmpDir := t.TempDir()
		relPath := "pkg/example.go"
		sourceCode := fmt.Sprintf(`package pkg

func %s(x int) int {
	return x + 1
}
`, exampleName)
		writeGoFile(t, tmpDir, relPath, sourceCode)

		// Build a graph with the example function node
		g := graph.New()
		exampleID := fmt.Sprintf("%s::%s", relPath, exampleName)
		g.AddNode(&graph.Node{
			ID:        exampleID,
			Kind:      graph.KindFunction,
			Name:      exampleName,
			FilePath:  relPath,
			StartLine: 3,
			EndLine:   5,
			Language:  "go",
		})

		engine := query.NewEngine(g)
		reader := &mockSourceReader{g: g, rootPath: tmpDir}

		result, err := GenerateScaffold(engine, reader, exampleID, newName)
		if err != nil {
			rt.Fatalf("GenerateScaffold returned error: %v", err)
		}

		if len(result.Edits) == 0 {
			rt.Fatal("expected at least one ScaffoldEdit, got none")
		}

		// Verify each edit has valid fields and the new name is present
		foundNewName := false
		for i, edit := range result.Edits {
			if edit.FilePath == "" {
				rt.Errorf("edit[%d].FilePath is empty", i)
			}
			if edit.InsertionLine <= 0 {
				rt.Errorf("edit[%d].InsertionLine = %d, want > 0", i, edit.InsertionLine)
			}
			if edit.Code == "" {
				rt.Errorf("edit[%d].Code is empty", i)
			}
			if strings.Contains(edit.Code, newName) {
				foundNewName = true
			}
		}

		if !foundNewName {
			rt.Errorf("no edit contains the new name %q; edits: %v", newName, result.Edits)
		}

		// The new_symbol edit should NOT contain the old example name as a
		// standalone identifier. A simple strings.Contains would mis-flag
		// the new name when it's a superstring (e.g. example="Bbb",
		// new="Bbba" — "Bbba" trivially contains "Bbb"). Use a word-
		// boundary match so "Bbb" only counts when it stands alone.
		oldRE := regexp.MustCompile(`\b` + regexp.QuoteMeta(exampleName) + `\b`)
		for _, edit := range result.Edits {
			if edit.Reason == "new_symbol" {
				if oldRE.MatchString(edit.Code) {
					rt.Errorf("new_symbol edit still contains old name %q", exampleName)
				}
			}
		}
	})
}

// --- Unit Tests for Edge Cases ---

// TestScaffoldNoCallersOmitsRegistration verifies that when the example symbol
// has no callers at depth 1, the registration/wiring section is omitted and a
// note is included explaining that no wiring pattern was detected.
func TestScaffoldNoCallersOmitsRegistration(t *testing.T) {
	tmpDir := t.TempDir()
	relPath := "pkg/lonely.go"
	sourceCode := `package pkg

func LonelyFunc(x int) int {
	return x * 2
}
`
	writeGoFile(t, tmpDir, relPath, sourceCode)

	g := graph.New()
	exampleID := "pkg/lonely.go::LonelyFunc"
	g.AddNode(&graph.Node{
		ID:        exampleID,
		Kind:      graph.KindFunction,
		Name:      "LonelyFunc",
		FilePath:  relPath,
		StartLine: 3,
		EndLine:   5,
		Language:  "go",
	})
	// No callers added — the function is isolated

	engine := query.NewEngine(g)
	reader := &mockSourceReader{g: g, rootPath: tmpDir}

	result, err := GenerateScaffold(engine, reader, exampleID, "NewFunc")
	require.NoError(t, err)

	// Should have no registration edit
	for _, edit := range result.Edits {
		assert.NotEqual(t, "registration", edit.Reason,
			"registration edit should be omitted when there are no callers")
	}

	// Should have a note about missing callers
	foundNote := false
	for _, note := range result.Notes {
		if strings.Contains(note, "no callers") || strings.Contains(note, "registration") || strings.Contains(note, "wiring") {
			foundNote = true
			break
		}
	}
	assert.True(t, foundNote, "expected a note about omitted registration; notes: %v", result.Notes)
}

// TestScaffoldNoTestsOmitsTestStub verifies that when the example symbol has
// no associated test file or test function, the test stub is omitted and a
// note is included explaining that no test pattern was detected.
func TestScaffoldNoTestsOmitsTestStub(t *testing.T) {
	tmpDir := t.TempDir()
	relPath := "pkg/notested.go"
	sourceCode := `package pkg

func NoTestedFunc(s string) string {
	return s + "!"
}
`
	writeGoFile(t, tmpDir, relPath, sourceCode)
	// Deliberately do NOT create a _test.go file

	g := graph.New()
	exampleID := "pkg/notested.go::NoTestedFunc"
	g.AddNode(&graph.Node{
		ID:        exampleID,
		Kind:      graph.KindFunction,
		Name:      "NoTestedFunc",
		FilePath:  relPath,
		StartLine: 3,
		EndLine:   5,
		Language:  "go",
	})

	engine := query.NewEngine(g)
	reader := &mockSourceReader{g: g, rootPath: tmpDir}

	result, err := GenerateScaffold(engine, reader, exampleID, "NewFunc")
	require.NoError(t, err)

	// Should have no test_stub edit
	for _, edit := range result.Edits {
		assert.NotEqual(t, "test_stub", edit.Reason,
			"test_stub edit should be omitted when there is no test file")
	}

	// Should have a note about missing test pattern
	foundNote := false
	for _, note := range result.Notes {
		if strings.Contains(note, "no test") || strings.Contains(note, "test stub") || strings.Contains(note, "test pattern") {
			foundNote = true
			break
		}
	}
	assert.True(t, foundNote, "expected a note about omitted test stub; notes: %v", result.Notes)
}
