package gitstore

import (
	"bufio"
	"context"
	"errors"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"testing"
	"time"

	git "github.com/go-git/go-git/v5"
	"github.com/go-git/go-git/v5/plumbing/object"
)

func TestRepositorySnapshotGrepAndReadUseExactCommit(t *testing.T) {
	repository, firstSHA, secondSHA := snapshotTestRepository(t)

	var firstMatches []RepositorySnapshotGrepMatch
	err := grepRepositorySnapshot(context.Background(), repository, RepositorySnapshotGrepRequest{
		CommitSHA: firstSHA,
		Pattern:   "managed model",
		PathGlobs: []string{"docs/policy.md"},
	}, func(match RepositorySnapshotGrepMatch) error {
		firstMatches = append(firstMatches, match)
		return nil
	})
	if err != nil {
		t.Fatalf("grep first snapshot: %v", err)
	}
	if len(firstMatches) != 1 || firstMatches[0].Path != "docs/policy.md" || firstMatches[0].Line != 2 {
		t.Fatalf("unexpected first matches: %#v", firstMatches)
	}
	if firstMatches[0].Preview != "The managed model is alpha." {
		t.Fatalf("unexpected first preview: %q", firstMatches[0].Preview)
	}

	var secondMatches []RepositorySnapshotGrepMatch
	err = grepRepositorySnapshot(context.Background(), repository, RepositorySnapshotGrepRequest{
		CommitSHA: secondSHA,
		Pattern:   "managed model",
		PathGlobs: []string{"docs/policy.md"},
	}, func(match RepositorySnapshotGrepMatch) error {
		secondMatches = append(secondMatches, match)
		return nil
	})
	if err != nil {
		t.Fatalf("grep second snapshot: %v", err)
	}
	if len(secondMatches) != 1 || secondMatches[0].Preview != "The managed model is beta." {
		t.Fatalf("unexpected second matches: %#v", secondMatches)
	}

	var chunks []RepositorySnapshotReadChunk
	err = readRepositorySnapshot(context.Background(), repository, RepositorySnapshotReadRequest{
		CommitSHA: firstSHA,
		Path:      "docs/policy.md",
		LineStart: 2,
		LineEnd:   2,
	}, func(chunk RepositorySnapshotReadChunk) error {
		chunks = append(chunks, chunk)
		return nil
	})
	if err != nil {
		t.Fatalf("read first snapshot: %v", err)
	}
	if len(chunks) != 1 || string(chunks[0].Content) != "The managed model is alpha.\n" {
		t.Fatalf("unexpected read chunks: %#v", chunks)
	}
}

func TestRepositorySnapshotReadPreservesBlankLines(t *testing.T) {
	repository, firstSHA, _ := snapshotTestRepository(t)
	var chunks []RepositorySnapshotReadChunk
	err := readRepositorySnapshot(context.Background(), repository, RepositorySnapshotReadRequest{
		CommitSHA: firstSHA,
		Path:      "docs/blank-lines.md",
		LineStart: 1,
		LineEnd:   3,
	}, func(chunk RepositorySnapshotReadChunk) error {
		chunks = append(chunks, chunk)
		return nil
	})
	if err != nil {
		t.Fatalf("read blank-line fixture: %v", err)
	}
	if len(chunks) != 3 {
		t.Fatalf("expected one chunk per requested line, got %#v", chunks)
	}
	var content strings.Builder
	for index, chunk := range chunks {
		line := index + 1
		if chunk.LineStart != line || chunk.LineEnd != line {
			t.Fatalf("unexpected coordinates for chunk %d: %#v", index, chunk)
		}
		content.Write(chunk.Content)
	}
	if content.String() != "first\n\nthird\n" {
		t.Fatalf("blank lines changed during read: %q", content.String())
	}
}

func TestRepositorySnapshotListsOnlyImmediateSafeEntries(t *testing.T) {
	repositoryPath, firstSHA, _ := snapshotTestRepository(t)
	var entries []RepositorySnapshotListEntry
	err := listRepositorySnapshot(context.Background(), repositoryPath, RepositorySnapshotListRequest{
		CommitSHA: firstSHA,
	}, func(entry RepositorySnapshotListEntry) error {
		entries = append(entries, entry)
		return nil
	})
	if err != nil {
		t.Fatalf("list snapshot root: %v", err)
	}
	if len(entries) != 1 || entries[0].Path != "docs" || entries[0].Kind != "directory" {
		t.Fatalf("unexpected root entries: %#v", entries)
	}

	entries = nil
	err = listRepositorySnapshot(context.Background(), repositoryPath, RepositorySnapshotListRequest{
		CommitSHA: firstSHA,
		Path:      "docs",
	}, func(entry RepositorySnapshotListEntry) error {
		entries = append(entries, entry)
		return nil
	})
	if err != nil {
		t.Fatalf("list snapshot docs: %v", err)
	}
	seen := make(map[string]string, len(entries))
	for _, entry := range entries {
		seen[entry.Path] = entry.Kind
	}
	if _, ok := seen["docs/link.md"]; ok || seen["docs/policy.md"] != "file" || seen["docs/blob.bin"] != "file" {
		t.Fatalf("unexpected docs entries: %#v", entries)
	}
}

func TestRepositorySnapshotListStopsProducerWhenConsumerIsFull(t *testing.T) {
	repositoryPath, firstSHA, _ := snapshotTestRepository(t)
	stop := errors.New("consumer full")
	emits := 0
	err := listRepositorySnapshot(context.Background(), repositoryPath, RepositorySnapshotListRequest{
		CommitSHA: firstSHA,
		Path:      "docs",
	}, func(RepositorySnapshotListEntry) error {
		emits++
		return stop
	})
	if !errors.Is(err, stop) {
		t.Fatalf("expected consumer stop, got %v", err)
	}
	if emits != 1 {
		t.Fatalf("expected one emitted entry, got %d", emits)
	}
}

func TestRepositorySnapshotRejectsTraversalAndSymlink(t *testing.T) {
	repository, firstSHA, _ := snapshotTestRepository(t)
	err := readRepositorySnapshot(context.Background(), repository, RepositorySnapshotReadRequest{
		CommitSHA: firstSHA,
		Path:      "../secret",
		LineStart: 1,
		LineEnd:   1,
	}, func(RepositorySnapshotReadChunk) error { return nil })
	if err == nil {
		t.Fatal("expected traversal to be rejected")
	}

	err = readRepositorySnapshot(context.Background(), repository, RepositorySnapshotReadRequest{
		CommitSHA: firstSHA,
		Path:      "docs/link.md",
		LineStart: 1,
		LineEnd:   1,
	}, func(RepositorySnapshotReadChunk) error { return nil })
	if !errors.Is(err, ErrRepositorySnapshotEscape) {
		t.Fatalf("expected symlink escape error, got %v", err)
	}
}

func TestRepositorySnapshotHonorsCancellation(t *testing.T) {
	repository, firstSHA, _ := snapshotTestRepository(t)
	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	err := grepRepositorySnapshot(ctx, repository, RepositorySnapshotGrepRequest{
		CommitSHA: firstSHA,
		Pattern:   "managed",
	}, func(RepositorySnapshotGrepMatch) error { return nil })
	if !errors.Is(err, context.Canceled) {
		t.Fatalf("expected context cancellation, got %v", err)
	}
}

func TestRepositorySnapshotSkipsBinaryContent(t *testing.T) {
	repository, firstSHA, _ := snapshotTestRepository(t)
	var matches []RepositorySnapshotGrepMatch
	err := grepRepositorySnapshot(context.Background(), repository, RepositorySnapshotGrepRequest{
		CommitSHA: firstSHA,
		Pattern:   "secret",
		PathGlobs: []string{"docs/blob.bin"},
	}, func(match RepositorySnapshotGrepMatch) error {
		matches = append(matches, match)
		return nil
	})
	if err != nil {
		t.Fatalf("grep binary snapshot: %v", err)
	}
	if len(matches) != 0 {
		t.Fatalf("binary content must not become knowledge evidence: %#v", matches)
	}

	err = readRepositorySnapshot(context.Background(), repository, RepositorySnapshotReadRequest{
		CommitSHA: firstSHA,
		Path:      "docs/blob.bin",
		LineStart: 1,
		LineEnd:   1,
	}, func(RepositorySnapshotReadChunk) error { return nil })
	if !errors.Is(err, ErrRepositorySnapshotEscape) {
		t.Fatalf("expected binary read to fail closed, got %v", err)
	}
}

func TestRepositorySnapshotRejectsBinaryContentAfterRequestedRange(t *testing.T) {
	repository, firstSHA, _ := snapshotTestRepository(t)
	emits := 0
	err := readRepositorySnapshot(context.Background(), repository, RepositorySnapshotReadRequest{
		CommitSHA: firstSHA,
		Path:      "docs/late-binary.bin",
		LineStart: 1,
		LineEnd:   1,
	}, func(RepositorySnapshotReadChunk) error {
		emits++
		return nil
	})
	if !errors.Is(err, ErrRepositorySnapshotEscape) {
		t.Fatalf("expected late binary content to fail closed, got %v", err)
	}
	if emits != 0 {
		t.Fatalf("binary content must be rejected before any range is emitted, got %d chunks", emits)
	}
}

func TestRepositorySnapshotReportsOversizedLineAsResourceExhausted(t *testing.T) {
	_, err := readSnapshotField(bufio.NewReader(strings.NewReader(strings.Repeat("a", repositorySnapshotMaxLineBytes+1))), '\n', repositorySnapshotMaxLineBytes)
	if !errors.Is(err, ErrRepositorySnapshotResourceExhausted) {
		t.Fatalf("expected resource exhaustion, got %v", err)
	}
}

func TestRepositorySnapshotRejectsOversizedBlob(t *testing.T) {
	err := validateSnapshotBlobSize([]byte(strconv.FormatInt(repositorySnapshotMaxBlobBytes+1, 10)))
	if !errors.Is(err, ErrRepositorySnapshotResourceExhausted) {
		t.Fatalf("expected oversized blob to exhaust resources, got %v", err)
	}
}

func TestRepositorySnapshotGitEnvironmentIsAllowlisted(t *testing.T) {
	t.Setenv("KNOWLEDGE_QA_TEST_SECRET", "must-not-reach-git")
	environment := snapshotGitEnvironment()
	allowed := map[string]struct{}{
		"PATH": {}, "HOME": {}, "XDG_CONFIG_HOME": {}, "GIT_CONFIG": {},
		"GIT_CONFIG_GLOBAL": {}, "GIT_CONFIG_SYSTEM": {}, "GIT_CONFIG_NOSYSTEM": {},
		"GIT_ATTR_NOSYSTEM": {}, "GIT_NO_LAZY_FETCH": {}, "GIT_NO_REPLACE_OBJECTS": {},
		"GIT_OPTIONAL_LOCKS": {}, "GIT_TERMINAL_PROMPT": {}, "LC_ALL": {},
	}
	for _, item := range environment {
		key, _, _ := strings.Cut(item, "=")
		if _, ok := allowed[key]; !ok {
			t.Fatalf("unexpected environment leaked to Git subprocess: %q", item)
		}
	}
	joined := strings.Join(environment, "\n")
	if !strings.Contains(joined, "HOME=/nonexistent") ||
		!strings.Contains(joined, "GIT_CONFIG=/dev/null") ||
		!strings.Contains(joined, "GIT_NO_LAZY_FETCH=1") {
		t.Fatalf("Git configuration isolation is incomplete: %#v", environment)
	}
}

func snapshotTestRepository(t *testing.T) (string, string, string) {
	t.Helper()
	root := t.TempDir()
	repository, err := git.PlainInit(root, false)
	if err != nil {
		t.Fatalf("initialize test repository: %v", err)
	}
	if err := os.MkdirAll(filepath.Join(root, "docs"), 0o755); err != nil {
		t.Fatalf("create docs directory: %v", err)
	}
	if err := os.WriteFile(filepath.Join(root, "docs", "policy.md"), []byte("# Policy\nThe managed model is alpha.\n"), 0o644); err != nil {
		t.Fatalf("write policy: %v", err)
	}
	if err := os.WriteFile(filepath.Join(root, "docs", "blank-lines.md"), []byte("first\n\nthird\n"), 0o644); err != nil {
		t.Fatalf("write blank-line fixture: %v", err)
	}
	if err := os.WriteFile(filepath.Join(root, "docs", "blob.bin"), []byte("secret\x00binary\n"), 0o644); err != nil {
		t.Fatalf("write binary fixture: %v", err)
	}
	if err := os.WriteFile(filepath.Join(root, "docs", "late-binary.bin"), []byte("visible first line\n"+strings.Repeat("a", repositorySnapshotBinaryProbeBytes)+"\x00binary\n"), 0o644); err != nil {
		t.Fatalf("write late binary fixture: %v", err)
	}
	if err := os.Symlink("../../outside", filepath.Join(root, "docs", "link.md")); err != nil {
		t.Fatalf("create symlink: %v", err)
	}
	worktree, err := repository.Worktree()
	if err != nil {
		t.Fatalf("open worktree: %v", err)
	}
	if _, err := worktree.Add("docs/policy.md"); err != nil {
		t.Fatalf("add policy: %v", err)
	}
	if _, err := worktree.Add("docs/blank-lines.md"); err != nil {
		t.Fatalf("add blank-line fixture: %v", err)
	}
	if _, err := worktree.Add("docs/link.md"); err != nil {
		t.Fatalf("add symlink: %v", err)
	}
	if _, err := worktree.Add("docs/blob.bin"); err != nil {
		t.Fatalf("add binary fixture: %v", err)
	}
	if _, err := worktree.Add("docs/late-binary.bin"); err != nil {
		t.Fatalf("add late binary fixture: %v", err)
	}
	first, err := worktree.Commit("first", &git.CommitOptions{
		Author: &object.Signature{Name: "test", Email: "test@example.com", When: time.Unix(1, 0).UTC()},
	})
	if err != nil {
		t.Fatalf("commit first snapshot: %v", err)
	}
	if err := os.WriteFile(filepath.Join(root, "docs", "policy.md"), []byte("# Policy\nThe managed model is beta.\n"), 0o644); err != nil {
		t.Fatalf("update policy: %v", err)
	}
	if _, err := worktree.Add("docs/policy.md"); err != nil {
		t.Fatalf("add updated policy: %v", err)
	}
	second, err := worktree.Commit("second", &git.CommitOptions{
		Author: &object.Signature{Name: "test", Email: "test@example.com", When: time.Unix(2, 0).UTC()},
	})
	if err != nil {
		t.Fatalf("commit second snapshot: %v", err)
	}
	return root, first.String(), second.String()
}
