package parser_test

import (
	"context"
	"fmt"
	"testing"

	contractevents "bit.admedia.com/scm/ad/adpilot-indexing-code-parser.com/internal/contracts/events"
	"bit.admedia.com/scm/ad/adpilot-indexing-code-parser.com/internal/contracts/graph"
	"bit.admedia.com/scm/ad/adpilot-indexing-code-parser.com/internal/service/parser"
	"bit.admedia.com/scm/ad/adpilot-indexing-code-parser.com/internal/store"
)

type mockFinalizeStore struct {
	artifacts              []graph.Artifact
	snapshotExists         bool
	savedGraph             graph.Artifact
	savedGraphID           string
	run                    parser.IndexingRunSnapshot
	docChunks              []parser.DocChunkRecord
	docSections            []parser.DocSectionRecord
	completedDocParseRuns  int
	syncedDocsParseCounter int
	processedCodeFilePaths int
	skippedCodeFilePaths   int
	snapshotCodeFiles      int
	codeArtifacts          int
	syncedCodeParseFanIn   bool
	recordCommitFailure    struct {
		repoID     string
		snapshotID string
		commitSHA  string
		eventID    string
		message    string
	}
}

func (m *mockFinalizeStore) SaveCodeChunks(context.Context, string, string, string, string, string, string, []contractevents.CodeChunk) error {
	return nil
}
func (m *mockFinalizeStore) SaveIndexingDiagnostics(context.Context, string, string, string, string, string, string, string, []contractevents.ParserDiagnostic) error {
	return nil
}
func (m *mockFinalizeStore) ListArtifactsBySnapshot(_ context.Context, _, _ string) ([]graph.Artifact, error) {
	return m.artifacts, nil
}
func (m *mockFinalizeStore) SaveSnapshotGraph(_ context.Context, artifact graph.Artifact, artifactID string) (string, string, error) {
	m.savedGraph = artifact
	m.savedGraphID = artifactID
	return artifactID, "mongo://snapshot_graphs/" + artifactID, nil
}
func (m *mockFinalizeStore) GetSnapshotGraphBySnapshot(context.Context, string, string) (graph.Artifact, string, bool, error) {
	return graph.Artifact{}, "", m.snapshotExists, nil
}
func (m *mockFinalizeStore) IncrementProcessedCodeFiles(context.Context, string, string, string) error {
	return nil
}
func (m *mockFinalizeStore) IncrementSkippedCodeFiles(context.Context, string, string, string) error {
	return nil
}
func (m *mockFinalizeStore) CountSkippedCodeFilePaths(context.Context, string, string) (int, error) {
	return m.skippedCodeFilePaths, nil
}
func (m *mockFinalizeStore) IncrementFailedCodeFiles(context.Context, string, string, string) error {
	return nil
}
func (m *mockFinalizeStore) RecordCommitChangedFailure(_ context.Context, repoID, snapshotID, commitSHA, eventID, message string) error {
	m.recordCommitFailure = struct {
		repoID     string
		snapshotID string
		commitSHA  string
		eventID    string
		message    string
	}{repoID, snapshotID, commitSHA, eventID, message}
	return nil
}
func (m *mockFinalizeStore) SyncCodeParseCounter(context.Context, string, string, int) error {
	return nil
}
func (m *mockFinalizeStore) CountProcessedCodeFilePaths(context.Context, string, string) (int, error) {
	if m.processedCodeFilePaths > 0 {
		return m.processedCodeFilePaths, nil
	}
	// In the correct case the distinct processed-path count equals the processed
	// counter; fall back to it when a test doesn't set paths explicitly.
	return m.run.ProcessedCodeFiles, nil
}
func (m *mockFinalizeStore) CountSnapshotCodeFiles(context.Context, string, string) (int, error) {
	return m.snapshotCodeFiles, nil
}
func (m *mockFinalizeStore) CountCodeArtifacts(context.Context, string, string) (int, error) {
	if m.codeArtifacts > 0 {
		return m.codeArtifacts, nil
	}
	// Default to the number of saved per-file artifacts the mock is holding, which
	// is exactly what ListArtifactsBySnapshot would merge.
	return len(m.artifacts), nil
}
func (m *mockFinalizeStore) SyncCodeParseFanIn(context.Context, string, string, float64) error {
	m.syncedCodeParseFanIn = true
	if m.run.ExpectedCodeFiles > 0 {
		m.run.ProcessedCodeFiles = m.run.ExpectedCodeFiles
	}
	return nil
}
func (m *mockFinalizeStore) CountCompletedDocParseRuns(context.Context, string, string) (int, error) {
	return m.completedDocParseRuns, nil
}
func (m *mockFinalizeStore) SyncDocsParseCounter(_ context.Context, _, _ string, processed int) error {
	m.syncedDocsParseCounter = processed
	m.run.ProcessedDocsFiles = processed
	return nil
}
func (m *mockFinalizeStore) GetIndexingRun(context.Context, string, string) (parser.IndexingRunSnapshot, error) {
	return m.run, nil
}
func (m *mockFinalizeStore) SetGraphFinalizeCompleted(context.Context, string, string) error {
	return nil
}
func (m *mockFinalizeStore) ListDocChunks(context.Context, string, string) ([]parser.DocChunkRecord, error) {
	return m.docChunks, nil
}
func (m *mockFinalizeStore) ListDocSections(context.Context, string, string) ([]parser.DocSectionRecord, error) {
	return m.docSections, nil
}

func (m *mockFinalizeStore) SaveArtifact(_ context.Context, artifact graph.Artifact) (string, string, error) {
	return "art_test", "mongo://code_graph_artifacts/art_test", nil
}
func (m *mockFinalizeStore) GetArtifact(context.Context, string) (graph.Artifact, error) {
	return graph.Artifact{}, nil
}
func (m *mockFinalizeStore) FindArtifactByCommitFile(context.Context, string, string, string) (graph.Artifact, string, error) {
	return graph.Artifact{}, "", nil
}
func (m *mockFinalizeStore) FindArtifactBySnapshotFile(context.Context, string, string, string) (graph.Artifact, string, error) {
	return graph.Artifact{}, "", nil
}
func (m *mockFinalizeStore) SaveDelta(context.Context, graph.Delta) (string, string, error) {
	return "delta_test", "mongo://graph_deltas/delta_test", nil
}

var _ parser.FinalizeStore = (*mockFinalizeStore)(nil)
var _ store.ArtifactStore = (*mockFinalizeStore)(nil)

func TestProcessRepoSnapshotReady_FinalizesMergedGraph(t *testing.T) {
	finalizeStore := &mockFinalizeStore{
		run: parser.IndexingRunSnapshot{
			RepoID:             "ad/example",
			SnapshotID:         "snap_finalize",
			CommitSHA:          "abc123",
			ExpectedCodeFiles:  1,
			ExpectedDocsFiles:  0,
			ProcessedCodeFiles: 1,
			ProcessedDocsFiles: 0,
		},
		artifacts: []graph.Artifact{
			{
				SchemaVersion: graph.SchemaVersionV1,
				RepoID:        "ad/example",
				SnapshotID:    "snap_finalize",
				CommitSHA:     "abc123",
				Nodes: []graph.Node{
					{ID: "file_main", Kind: graph.NodeKindFile, Name: "main.go", Path: "main.go"},
				},
			},
		},
	}
	pub := &mockPublisher{}
	svc := parser.NewService(parser.ServiceConfig{
		WorkspacePath: t.TempDir(),
		Workers:       1,
		ParserVersion: "v0.1.0-test",
	}, nil, pub, finalizeStore, parser.WithFinalizeStore(finalizeStore))

	event := contractevents.NewRepoSnapshotReadyEvent(
		"evt_snap_finalize",
		"ad/example",
		"snap_finalize",
		"abc123",
		"master",
	)
	event.FileCount = 1

	if err := svc.ProcessRepoSnapshotReady(context.Background(), event); err != nil {
		t.Fatalf("ProcessRepoSnapshotReady: %v", err)
	}

	if finalizeStore.savedGraphID != "art_snap_finalize_merged" {
		t.Fatalf("artifact id: got %q, want art_snap_finalize_merged", finalizeStore.savedGraphID)
	}
	_, graphs, _, _ := pub.counts()
	if graphs != 1 {
		t.Fatalf("expected 1 merged graph.artifact.ready publish, got %d", graphs)
	}
}

func TestProcessRepoSnapshotReady_DefersUntilArtifactsCatchUp(t *testing.T) {
	// Regression for the premature-seal / under-indexing bug: the fan-in path
	// accounting reports complete (processedCodeFilePaths == ExpectedCodeFiles)
	// while only a fraction of the per-file code_graph_artifacts have been saved.
	// Finalize MUST NOT seal a truncated graph here — it must defer until the
	// artifacts (the merge source) catch up.
	artifacts := make([]graph.Artifact, 25) // only 25 of 809 parsed so far
	for i := range artifacts {
		artifacts[i] = graph.Artifact{
			SchemaVersion: graph.SchemaVersionV1,
			RepoID:        "ad/store.com",
			SnapshotID:    "snap_lag",
			CommitSHA:     "abc123",
			Nodes: []graph.Node{
				{ID: fmt.Sprintf("file_%d", i), Kind: graph.NodeKindFile, Name: "main.go", Path: "main.go"},
			},
		}
	}
	finalizeStore := &mockFinalizeStore{
		run: parser.IndexingRunSnapshot{
			RepoID:             "ad/store.com",
			SnapshotID:         "snap_lag",
			CommitSHA:          "abc123",
			ExpectedCodeFiles:  809,
			ProcessedCodeFiles: 809,
		},
		processedCodeFilePaths: 809, // fan-in says "done"
		snapshotCodeFiles:      809,
		artifacts:              artifacts, // but only 25 artifacts exist
	}
	pub := &mockPublisher{}
	svc := parser.NewService(parser.ServiceConfig{
		WorkspacePath: t.TempDir(),
		Workers:       1,
		ParserVersion: "v0.1.0-test",
	}, nil, pub, finalizeStore, parser.WithFinalizeStore(finalizeStore))

	event := contractevents.NewRepoSnapshotReadyEvent(
		"evt_snap_lag", "ad/store.com", "snap_lag", "abc123", "master",
	)
	event.FileCount = 809

	if err := svc.ProcessRepoSnapshotReady(context.Background(), event); err != nil {
		t.Fatalf("ProcessRepoSnapshotReady: %v", err)
	}

	if finalizeStore.savedGraphID != "" {
		t.Fatalf("expected NO finalize while artifacts lag, but sealed graph %q", finalizeStore.savedGraphID)
	}
	if _, graphs, _, _ := pub.counts(); graphs != 0 {
		t.Fatalf("expected 0 merged graph publishes while artifacts lag, got %d", graphs)
	}
}

func TestProcessRepoSnapshotReady_FinalizesDespiteSkippedFiles(t *testing.T) {
	// Some code files are skipped/unsupported: they count toward ExpectedCodeFiles
	// (via the fan-in) but never produce a code_graph_artifact. The artifact target
	// must subtract them so finalize can still seal — otherwise it waits forever for
	// artifacts that will never exist (the publicgood.me 494/500 stall).
	const expected = 100
	const skipped = 6
	artifacts := make([]graph.Artifact, expected-skipped) // 94 real artifacts
	for i := range artifacts {
		artifacts[i] = graph.Artifact{
			SchemaVersion: graph.SchemaVersionV1,
			RepoID:        "github:org/repo",
			SnapshotID:    "snap_skip",
			CommitSHA:     "abc123",
			Nodes: []graph.Node{
				{ID: fmt.Sprintf("file_%d", i), Kind: graph.NodeKindFile, Name: "main.ts", Path: fmt.Sprintf("f%d.ts", i)},
			},
		}
	}
	finalizeStore := &mockFinalizeStore{
		run: parser.IndexingRunSnapshot{
			RepoID:             "github:org/repo",
			SnapshotID:         "snap_skip",
			CommitSHA:          "abc123",
			ExpectedCodeFiles:  expected,
			ProcessedCodeFiles: expected,
		},
		processedCodeFilePaths: expected, // fan-in complete (all files terminal)
		skippedCodeFilePaths:   skipped,  // 6 counted but produced no artifact
		snapshotCodeFiles:      expected,
		artifacts:              artifacts, // 94 == expected - skipped
	}
	pub := &mockPublisher{}
	svc := parser.NewService(parser.ServiceConfig{
		WorkspacePath: t.TempDir(),
		Workers:       1,
		ParserVersion: "v0.1.0-test",
	}, nil, pub, finalizeStore, parser.WithFinalizeStore(finalizeStore))

	event := contractevents.NewRepoSnapshotReadyEvent(
		"evt_snap_skip", "github:org/repo", "snap_skip", "abc123", "main",
	)
	event.FileCount = expected

	if err := svc.ProcessRepoSnapshotReady(context.Background(), event); err != nil {
		t.Fatalf("ProcessRepoSnapshotReady: %v", err)
	}

	if finalizeStore.savedGraphID == "" {
		t.Fatal("expected finalize to seal (artifacts == expected - skipped), but it deferred")
	}
	if _, graphs, _, _ := pub.counts(); graphs != 1 {
		t.Fatalf("expected 1 merged graph publish, got %d", graphs)
	}
}

func TestProcessRepoSnapshotReady_SyncsDocsCounterBeforeFinalize(t *testing.T) {
	finalizeStore := &mockFinalizeStore{
		run: parser.IndexingRunSnapshot{
			RepoID:             "ad/example",
			SnapshotID:         "snap_docs_sync",
			CommitSHA:          "abc123",
			ExpectedCodeFiles:  1,
			ExpectedDocsFiles:  5,
			ProcessedCodeFiles: 1,
			ProcessedDocsFiles: 4,
		},
		completedDocParseRuns: 5,
		artifacts: []graph.Artifact{
			{
				SchemaVersion: graph.SchemaVersionV1,
				RepoID:        "ad/example",
				SnapshotID:    "snap_docs_sync",
				CommitSHA:     "abc123",
				Nodes: []graph.Node{
					{ID: "file_main", Kind: graph.NodeKindFile, Name: "main.go", Path: "main.go"},
				},
			},
		},
	}
	pub := &mockPublisher{}
	svc := parser.NewService(parser.ServiceConfig{
		WorkspacePath: t.TempDir(),
		Workers:       1,
		ParserVersion: "v0.1.0-test",
	}, nil, pub, finalizeStore, parser.WithFinalizeStore(finalizeStore))

	event := contractevents.NewRepoSnapshotReadyEvent(
		"evt_snap_docs_sync",
		"ad/example",
		"snap_docs_sync",
		"abc123",
		"master",
	)

	if err := svc.ProcessRepoSnapshotReady(context.Background(), event); err != nil {
		t.Fatalf("ProcessRepoSnapshotReady: %v", err)
	}

	if finalizeStore.syncedDocsParseCounter != 5 {
		t.Fatalf("synced docs counter: got %d, want 5", finalizeStore.syncedDocsParseCounter)
	}
	if finalizeStore.savedGraphID != "art_snap_docs_sync_merged" {
		t.Fatalf("artifact id: got %q, want art_snap_docs_sync_merged", finalizeStore.savedGraphID)
	}
}

func TestProcessRepoSnapshotReady_SyncsCodeParseCounterFromPaths(t *testing.T) {
	artifacts := make([]graph.Artifact, 942)
	for i := range artifacts {
		artifacts[i] = graph.Artifact{
			SchemaVersion: graph.SchemaVersionV1,
			RepoID:        "ad/search.com",
			SnapshotID:    "snap_search",
			CommitSHA:     "abc123",
			Nodes: []graph.Node{
				{ID: fmt.Sprintf("file_%d", i), Kind: graph.NodeKindFile, Name: "main.go", Path: "main.go"},
			},
		}
	}

	finalizeStore := &mockFinalizeStore{
		run: parser.IndexingRunSnapshot{
			RepoID:             "ad/search.com",
			SnapshotID:         "snap_search",
			CommitSHA:          "abc123",
			ExpectedCodeFiles:  952,
			ExpectedDocsFiles:  0,
			ProcessedCodeFiles: 900,
			ProcessedDocsFiles: 0,
			FailedFiles:        10,
		},
		processedCodeFilePaths: 952,
		snapshotCodeFiles:      952,
		artifacts:              artifacts,
	}
	pub := &mockPublisher{}
	svc := parser.NewService(parser.ServiceConfig{
		WorkspacePath: t.TempDir(),
		Workers:       1,
		ParserVersion: "v0.1.0-test",
	}, nil, pub, finalizeStore, parser.WithFinalizeStore(finalizeStore))

	event := contractevents.NewRepoSnapshotReadyEvent(
		"evt_snap_search",
		"ad/search.com",
		"snap_search",
		"abc123",
		"master",
	)

	if err := svc.ProcessRepoSnapshotReady(context.Background(), event); err != nil {
		t.Fatalf("ProcessRepoSnapshotReady: %v", err)
	}

	if !finalizeStore.syncedCodeParseFanIn {
		t.Fatal("expected SyncCodeParseFanIn to run")
	}
	if finalizeStore.savedGraphID != "art_snap_search_merged" {
		t.Fatalf("artifact id: got %q, want art_snap_search_merged", finalizeStore.savedGraphID)
	}
}

func TestProcessRepoSnapshotReady_SyncsCodeParseFanInWhenZeroExpected(t *testing.T) {
	finalizeStore := &mockFinalizeStore{
		run: parser.IndexingRunSnapshot{
			RepoID:            "bitbucket_server:AD/api.search.com",
			SnapshotID:        "snap_zero",
			CommitSHA:         "abc123",
			ExpectedCodeFiles: 0,
			ExpectedDocsFiles: 0,
		},
	}
	pub := &mockPublisher{}
	svc := parser.NewService(parser.ServiceConfig{
		WorkspacePath: t.TempDir(),
		Workers:       1,
		ParserVersion: "v0.1.0-test",
	}, nil, pub, finalizeStore, parser.WithFinalizeStore(finalizeStore))

	event := contractevents.NewRepoSnapshotReadyEvent(
		"evt_snap_zero",
		"bitbucket_server:AD/api.search.com",
		"snap_zero",
		"abc123",
		"master",
	)

	if err := svc.ProcessRepoSnapshotReady(context.Background(), event); err != nil {
		t.Fatalf("ProcessRepoSnapshotReady: %v", err)
	}

	if !finalizeStore.syncedCodeParseFanIn {
		t.Fatal("expected SyncCodeParseFanIn when ExpectedCodeFiles is zero")
	}
}

func TestRecordCommitChangedFailure(t *testing.T) {
	finalizeStore := &mockFinalizeStore{}
	svc := parser.NewService(parser.ServiceConfig{}, nil, nil, nil, parser.WithFinalizeStore(finalizeStore))

	event := contractevents.NewCommitsChangedEvent(
		"evt_commit_fail",
		"ad/api.search.com",
		"snap_test",
		"abc123",
	)
	if err := svc.RecordCommitChangedFailure(context.Background(), event, "save delta failed"); err != nil {
		t.Fatalf("RecordCommitChangedFailure: %v", err)
	}
	if finalizeStore.recordCommitFailure.repoID != event.RepoID {
		t.Fatalf("repo_id: got %q", finalizeStore.recordCommitFailure.repoID)
	}
	if finalizeStore.recordCommitFailure.message != "save delta failed" {
		t.Fatalf("message: got %q", finalizeStore.recordCommitFailure.message)
	}
}
