package commitintel_test

import (
	"context"
	"errors"
	"os"
	"path/filepath"
	"testing"
	"time"

	"bit.admedia.com/scm/ad/adpilot-indexing-commit-intel.com/internal/contracts/events"
	"bit.admedia.com/scm/ad/adpilot-indexing-commit-intel.com/internal/contracts/graph"
	"bit.admedia.com/scm/ad/adpilot-indexing-commit-intel.com/internal/contracts/metadata"
	"bit.admedia.com/scm/ad/adpilot-indexing-commit-intel.com/internal/service/commitintel"
	"bit.admedia.com/scm/ad/adpilot-indexing-commit-intel.com/internal/store"
)

type mockDeltaReader struct {
	delta graph.Delta
	err   error
}

func (m *mockDeltaReader) LoadArtifactByURI(context.Context, string) (graph.Artifact, error) {
	return graph.Artifact{}, errors.New("not implemented")
}

func (m *mockDeltaReader) LoadDeltaByURI(context.Context, string) (graph.Delta, error) {
	if m.err != nil {
		return graph.Delta{}, m.err
	}
	return m.delta, nil
}

func TestProcessGraphDeltaReadySuccess(t *testing.T) {
	jobs := store.NewAnalysisStore()
	reader := &mockDeltaReader{delta: sampleDelta()}
	svc := commitintel.NewService(nil, reader, commitintel.NewDeltaImpactAnalyzer(), jobs)

	event := validGraphDeltaEvent(t, "file:///data/delta.json")

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

	analysisID := commitintel.AnalysisIDFromEvent(event)
	result, ok := jobs.GetResult(analysisID)
	if !ok {
		t.Fatalf("expected result for %s", analysisID)
	}
	if result.CommitSHA != event.TargetCommitSHA {
		t.Fatalf("commit_sha: got %q", result.CommitSHA)
	}
	if len(result.ImpactedSymbols) == 0 {
		t.Fatal("expected impacted symbols")
	}

	job, ok := jobs.GetJob(commitintel.JobIDFromEvent(event))
	if !ok {
		t.Fatal("expected job record")
	}
	if job.Status != store.JobStatusCompleted {
		t.Fatalf("job status: got %q", job.Status)
	}
}

func TestProcessGraphDeltaReadyIdempotent(t *testing.T) {
	jobs := store.NewAnalysisStore()
	reader := &mockDeltaReader{delta: sampleDelta()}
	svc := commitintel.NewService(nil, reader, commitintel.NewDeltaImpactAnalyzer(), jobs)

	event := validGraphDeltaEvent(t, "file:///data/delta.json")
	if err := svc.ProcessGraphDeltaReady(context.Background(), event); err != nil {
		t.Fatalf("first call: %v", err)
	}

	before, _ := jobs.GetResult(commitintel.AnalysisIDFromEvent(event))
	if err := svc.ProcessGraphDeltaReady(context.Background(), event); err != nil {
		t.Fatalf("second call: %v", err)
	}
	after, _ := jobs.GetResult(commitintel.AnalysisIDFromEvent(event))
	if before.AnalysisID != after.AnalysisID {
		t.Fatal("expected idempotent processing without duplicate result")
	}
}

func TestProcessGraphDeltaReadyLoadError(t *testing.T) {
	jobs := store.NewAnalysisStore()
	reader := &mockDeltaReader{err: store.ErrArtifactNotFound}
	svc := commitintel.NewService(nil, reader, commitintel.NewDeltaImpactAnalyzer(), jobs)

	event := validGraphDeltaEvent(t, "file:///data/missing.json")
	if err := svc.ProcessGraphDeltaReady(context.Background(), event); err == nil {
		t.Fatal("expected error")
	}

	job, ok := jobs.GetJob(commitintel.JobIDFromEvent(event))
	if !ok {
		t.Fatal("expected job record")
	}
	if job.Status != store.JobStatusFailed {
		t.Fatalf("job status: got %q", job.Status)
	}
}

type mockAnalysisWriter struct {
	saved     bool
	saveErr   error
	exists    bool
	existsErr error
}

func (m *mockAnalysisWriter) ExistsByEventID(context.Context, string) (bool, error) {
	return m.exists, m.existsErr
}

func (m *mockAnalysisWriter) SaveAnalysis(context.Context, store.CommitAnalysisDoc) error {
	if m.saveErr != nil {
		return m.saveErr
	}
	m.saved = true
	return nil
}

type gatedPublisher struct {
	saveGate *mockAnalysisWriter
}

func (p *gatedPublisher) PublishCommitAnalysisReady(context.Context, events.CommitAnalysisReadyEvent) error {
	if p.saveGate == nil || !p.saveGate.saved {
		return errors.New("publish before persist")
	}
	return nil
}

func TestProcessGraphDeltaReadyPersistBeforePublish(t *testing.T) {
	jobs := store.NewAnalysisStore()
	reader := &mockDeltaReader{delta: sampleDelta()}
	writer := &mockAnalysisWriter{}
	svc := commitintel.NewService(
		&gatedPublisher{saveGate: writer},
		reader,
		commitintel.NewDeltaImpactAnalyzer(),
		jobs,
		commitintel.WithAnalysisWriter(writer),
	)

	event := validGraphDeltaEvent(t, "mongo://graph_deltas/delta_target456_a1b2")
	if err := svc.ProcessGraphDeltaReady(context.Background(), event); err != nil {
		t.Fatalf("ProcessGraphDeltaReady: %v", err)
	}
	if !writer.saved {
		t.Fatal("expected analysis persisted before publish")
	}
}

func TestProcessGraphDeltaReadyMongoIdempotency(t *testing.T) {
	jobs := store.NewAnalysisStore()
	reader := &mockDeltaReader{delta: sampleDelta()}
	writer := &mockAnalysisWriter{exists: true}
	svc := commitintel.NewService(
		nil,
		reader,
		commitintel.NewDeltaImpactAnalyzer(),
		jobs,
		commitintel.WithAnalysisWriter(writer),
	)

	event := validGraphDeltaEvent(t, "mongo://graph_deltas/delta_target456_a1b2")
	if err := svc.ProcessGraphDeltaReady(context.Background(), event); err != nil {
		t.Fatalf("ProcessGraphDeltaReady: %v", err)
	}
	if writer.saved {
		t.Fatal("expected skip without save when event already exists in mongo")
	}
}

func TestProcessGraphDeltaReadyValidationError(t *testing.T) {
	jobs := store.NewAnalysisStore()
	svc := commitintel.NewService(nil, &mockDeltaReader{}, commitintel.NewDeltaImpactAnalyzer(), jobs)

	event := events.GraphDeltaReadyEvent{
		Envelope: metadata.Envelope{
			EventID:      "evt_bad",
			EventVersion: metadata.EventVersionV1,
			CreatedAt:    time.Now().UTC(),
		},
		RepoID: "ad/example",
	}

	if err := svc.ProcessGraphDeltaReady(context.Background(), event); err == nil {
		t.Fatal("expected validation error")
	}
}

func TestProcessGraphDeltaReadyIntegrationWithFilesystemReader(t *testing.T) {
	dir := t.TempDir()
	fixture, err := os.ReadFile(filepath.Join("..", "..", "store", "testdata", "delta_sample.json"))
	if err != nil {
		t.Fatalf("read fixture: %v", err)
	}
	deltaPath := filepath.Join(dir, "delta_target456_a1b2.json")
	if err := os.WriteFile(deltaPath, fixture, 0o644); err != nil {
		t.Fatalf("write fixture: %v", err)
	}

	reader, err := store.NewFilesystemReader(dir)
	if err != nil {
		t.Fatalf("NewFilesystemReader: %v", err)
	}

	jobs := store.NewAnalysisStore()
	svc := commitintel.NewService(nil, reader, commitintel.NewDeltaImpactAnalyzer(), jobs)

	event := validGraphDeltaEvent(t, "file://"+deltaPath)
	if err := svc.ProcessGraphDeltaReady(context.Background(), event); err != nil {
		t.Fatalf("ProcessGraphDeltaReady: %v", err)
	}

	result, ok := jobs.GetResult(commitintel.AnalysisIDFromEvent(event))
	if !ok {
		t.Fatal("expected stored result")
	}
	if result.Summary == "" {
		t.Fatal("expected summary")
	}
}

func validGraphDeltaEvent(t *testing.T, deltaURI string) events.GraphDeltaReadyEvent {
	t.Helper()
	return events.NewGraphDeltaReadyEvent(
		"evt_delta_test",
		"ad/example",
		"base1234567890",
		"target4567890ab",
		"delta_target456_a1b2",
		deltaURI,
		"v1",
	)
}

func sampleDelta() graph.Delta {
	return graph.Delta{
		SchemaVersion:   graph.DeltaSchemaVersionV1,
		RepoID:          "ad/example",
		BaseCommitSHA:   "base1234567890",
		TargetCommitSHA: "target4567890ab",
		ChangedFiles:    []string{"internal/main.go"},
		FileDeltas: []graph.FileDelta{
			{
				FilePath: "internal/main.go",
				AddedNodes: []graph.Node{
					{Kind: graph.NodeKindSymbol, Name: "Run"},
				},
				RemovedNodes: []graph.Node{
					{Kind: graph.NodeKindSymbol, Name: "Helper"},
				},
			},
		},
	}
}
