package parser_test

import (
	"context"
	"errors"
	"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"
)

func artifactForCommit(repoID, snapshotID, commitSHA, filePath string, extraNodes []graph.Node) graph.Artifact {
	nodes := []graph.Node{
		{ID: "file:" + filePath, Kind: graph.NodeKindFile, Name: filePath, Path: filePath, Language: "go"},
	}
	nodes = append(nodes, extraNodes...)
	return graph.Artifact{
		SchemaVersion: graph.SchemaVersionV1,
		RepoID:        repoID,
		SnapshotID:    snapshotID,
		CommitSHA:     commitSHA,
		Nodes:         nodes,
	}
}

func TestProcessCommitsChangedPublishesDelta(t *testing.T) {
	workspace := setupWorkspace(t, nil)
	storeMock := newMockArtifactStore()
	pub := &mockPublisher{}
	finalize := &mockFinalizeStore{
		run: parser.IndexingRunSnapshot{
			RepoID:             "ad/example",
			SnapshotID:         "snap_1",
			CommitSHA:          "target_sha",
			ExpectedCodeFiles:  1,
			ProcessedCodeFiles: 1,
		},
	}

	baseArtifact := artifactForCommit("ad/example", "snap_old", "base_sha", "main.go", []graph.Node{
		{ID: "symbol:main.go#helper", Kind: graph.NodeKindSymbol, Name: "helper", Path: "main.go"},
	})
	targetArtifact := artifactForCommit("ad/example", "snap_1", "target_sha", "main.go", []graph.Node{
		{ID: "symbol:main.go#run", Kind: graph.NodeKindSymbol, Name: "run", Path: "main.go"},
	})

	storeMock.byCommit[commitFileKey("ad/example", "base_sha", "main.go")] = baseArtifact
	storeMock.byCommit[commitFileKey("ad/example", "target_sha", "main.go")] = targetArtifact

	svc := parser.NewService(parser.ServiceConfig{
		WorkspacePath: workspace,
		Workers:       1,
		ParserVersion: "v0.1.0-test",
	}, nil, pub, storeMock, parser.WithFinalizeStore(finalize))

	event := contractevents.NewCommitsChangedEvent("evt_commit_1", "ad/example", "snap_1", "target_sha")
	event.ParentSHA = "base_sha"
	event.ChangedFiles = []string{"main.go"}
	event.AuthorName = "Alice"
	event.AuthorEmail = "alice@example.com"
	event.CommitMessage = "Update main.go"

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

	jobID := parser.CommitJobIDFromEvent(event)
	job, ok := svc.GetParseJob(jobID)
	if !ok {
		t.Fatal("expected commit job")
	}
	if job.Status != parser.JobStatusCompleted {
		t.Fatalf("status: got %q, want completed", job.Status)
	}

	_, _, deltas, diags := pub.counts()
	if deltas != 1 {
		t.Fatalf("expected 1 graph.delta.ready publish, got %d", deltas)
	}
	if diags != 0 {
		t.Fatalf("expected 0 diagnostics, got %d", diags)
	}
	if len(storeMock.deltas) != 1 {
		t.Fatalf("expected 1 saved delta, got %d", len(storeMock.deltas))
	}
	if len(storeMock.deltas[0].FileDeltas) != 1 {
		t.Fatalf("file deltas: %+v", storeMock.deltas[0].FileDeltas)
	}
	if storeMock.deltas[0].GitCommit.AuthorName != "Alice" {
		t.Fatalf("git commit metadata: %+v", storeMock.deltas[0].GitCommit)
	}
	if pub.lastGraphDelta.AuthorName != "Alice" {
		t.Fatalf("graph delta event metadata: %+v", pub.lastGraphDelta.GitCommitMetadata)
	}
}

func TestProcessCommitsChangedRootCommit(t *testing.T) {
	workspace := setupWorkspace(t, nil)
	storeMock := newMockArtifactStore()
	pub := &mockPublisher{}

	headArtifact := artifactForCommit("ad/example", "snap_1", "head_sha", "main.go", []graph.Node{
		{ID: "symbol:main.go#main", Kind: graph.NodeKindSymbol, Name: "main", Path: "main.go"},
	})
	storeMock.byCommit[commitFileKey("ad/example", "head_sha", "main.go")] = headArtifact

	finalize := &mockFinalizeStore{
		run: parser.IndexingRunSnapshot{
			RepoID:             "ad/example",
			SnapshotID:         "snap_1",
			CommitSHA:          "target_sha",
			ExpectedCodeFiles:  1,
			ProcessedCodeFiles: 1,
		},
	}

	svc := parser.NewService(parser.ServiceConfig{
		WorkspacePath: workspace,
		Workers:       1,
		ParserVersion: "v0.1.0-test",
	}, nil, pub, storeMock, parser.WithFinalizeStore(finalize))

	event := contractevents.NewCommitsChangedEvent("evt_commit_root", "ad/example", "snap_1", "target_sha")
	event.ChangedFiles = []string{"main.go"}

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

	if len(storeMock.deltas) != 1 {
		t.Fatalf("expected 1 saved delta, got %d", len(storeMock.deltas))
	}
	delta := storeMock.deltas[0]
	if delta.BaseCommitSHA != parser.GitEmptyTreeSHA {
		t.Fatalf("base commit sha: got %q, want empty tree", delta.BaseCommitSHA)
	}
	if len(delta.FileDeltas) != 1 || len(delta.FileDeltas[0].AddedNodes) < 1 {
		t.Fatalf("file deltas: %+v", delta.FileDeltas)
	}
	if len(delta.FileDeltas[0].RemovedNodes) != 0 {
		t.Fatalf("expected no removed nodes for root commit, got %+v", delta.FileDeltas[0].RemovedNodes)
	}
}

func TestProcessCommitsChangedWaitsForCodeParse(t *testing.T) {
	workspace := setupWorkspace(t, nil)
	storeMock := newMockArtifactStore()
	pub := &mockPublisher{}
	finalize := &mockFinalizeStore{
		run: parser.IndexingRunSnapshot{
			RepoID:             "ad/example",
			SnapshotID:         "snap_1",
			ExpectedCodeFiles:  2,
			ProcessedCodeFiles: 1,
		},
	}

	svc := parser.NewService(parser.ServiceConfig{
		WorkspacePath: workspace,
		Workers:       1,
	}, nil, pub, storeMock, parser.WithFinalizeStore(finalize))

	event := contractevents.NewCommitsChangedEvent("evt_commit_wait", "ad/example", "snap_1", "target_sha")
	event.ParentSHA = "base_sha"
	event.ChangedFiles = []string{"main.go"}

	err := svc.ProcessCommitsChanged(context.Background(), event)
	if !errors.Is(err, parser.ErrCommitArtifactsNotReady) {
		t.Fatalf("expected ErrCommitArtifactsNotReady, got %v", err)
	}
}

func TestProcessCommitsChangedMissingArtifacts(t *testing.T) {
	workspace := setupWorkspace(t, nil)
	storeMock := newMockArtifactStore()
	pub := &mockPublisher{}
	finalize := &mockFinalizeStore{
		run: parser.IndexingRunSnapshot{
			RepoID:             "ad/example",
			SnapshotID:         "snap_1",
			CommitSHA:          "target_sha",
			ExpectedCodeFiles:  1,
			ProcessedCodeFiles: 1,
		},
	}

	svc := parser.NewService(parser.ServiceConfig{
		WorkspacePath: workspace,
		Workers:       1,
	}, nil, pub, storeMock, parser.WithFinalizeStore(finalize))

	event := contractevents.NewCommitsChangedEvent("evt_commit_2", "ad/example", "snap_1", "target_sha")
	event.ParentSHA = "base_sha"
	event.ChangedFiles = []string{"main.go"}

	err := svc.ProcessCommitsChanged(context.Background(), event)
	if !errors.Is(err, parser.ErrCommitArtifactsNotReady) {
		t.Fatalf("expected ErrCommitArtifactsNotReady, got %v", err)
	}

	_, _, deltas, diags := pub.counts()
	if deltas != 0 {
		t.Fatalf("expected 0 delta publishes, got %d", deltas)
	}
	if diags != 0 {
		t.Fatalf("expected 0 diagnostics for retryable artifact wait, got %d", diags)
	}
}

type mockCommitOrderStore struct {
	completed []struct {
		repoID, snapshotID, commitSHA, parentSHA string
	}
}

func (m *mockCommitOrderStore) ParentCommitReady(context.Context, string, string, string) (bool, error) {
	return true, nil
}

func (m *mockCommitOrderStore) MarkCommitCompleted(_ context.Context, repoID, snapshotID, commitSHA, parentSHA string) error {
	m.completed = append(m.completed, struct {
		repoID, snapshotID, commitSHA, parentSHA string
	}{repoID, snapshotID, commitSHA, parentSHA})
	return nil
}

func (m *mockCommitOrderStore) SyncCommitFanIn(context.Context, string, string) error {
	return nil
}

func TestProcessCommitsChangedPublishesNonCodeDelta(t *testing.T) {
	workspace := setupWorkspace(t, nil)
	storeMock := newMockArtifactStore()
	pub := &mockPublisher{}
	commitOrder := &mockCommitOrderStore{}
	finalize := &mockFinalizeStore{
		run: parser.IndexingRunSnapshot{
			RepoID:             "ad/example",
			SnapshotID:         "snap_1",
			CommitSHA:          "target_sha",
			ExpectedCodeFiles:  1,
			ProcessedCodeFiles: 1,
		},
	}

	svc := parser.NewService(parser.ServiceConfig{
		WorkspacePath: workspace,
		Workers:       1,
	}, nil, pub, storeMock, parser.WithFinalizeStore(finalize), parser.WithCommitOrderStore(commitOrder))

	event := contractevents.NewCommitsChangedEvent("evt_commit_3", "ad/example", "snap_1", "target_sha")
	event.ParentSHA = "base_sha"
	event.ChangedFiles = []string{"docs/readme.md", "config/app.yaml", "Makefile"}
	event.CommitMessage = "Update docs and ignore rules"

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

	jobID := parser.CommitJobIDFromEvent(event)
	job, ok := svc.GetParseJob(jobID)
	if !ok {
		t.Fatal("expected commit job")
	}
	if job.Status != parser.JobStatusCompleted {
		t.Fatalf("status: got %q, want completed", job.Status)
	}
	if len(commitOrder.completed) != 1 {
		t.Fatalf("expected 1 MarkCommitCompleted call, got %d", len(commitOrder.completed))
	}
	if len(storeMock.deltas) != 1 {
		t.Fatalf("expected 1 saved delta, got %d", len(storeMock.deltas))
	}
	delta := storeMock.deltas[0]
	if len(delta.FileDeltas) != 0 {
		t.Fatalf("expected empty file deltas for non-code commit, got %+v", delta.FileDeltas)
	}
	if len(delta.ChangedFiles) != 3 {
		t.Fatalf("changed_files: %+v", delta.ChangedFiles)
	}

	_, _, deltas, diags := pub.counts()
	if deltas != 1 || diags != 0 {
		t.Fatalf("expected 1 delta publish, got deltas=%d diags=%d", deltas, diags)
	}
}
