package parser_test

import (
	"context"
	"errors"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"sync/atomic"
	"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 mockAnalyzer struct {
	language string
	fn       func(ctx context.Context, source []byte, filePath string) (*graph.Artifact, []contractevents.CodeChunk, error)
	calls    atomic.Int32
}

func (m *mockAnalyzer) SupportedLanguage() string {
	return m.language
}

func (m *mockAnalyzer) ParseFile(ctx context.Context, source []byte, filePath string) (*graph.Artifact, []contractevents.CodeChunk, error) {
	m.calls.Add(1)
	if m.fn != nil {
		return m.fn(ctx, source, filePath)
	}
	return &graph.Artifact{
		SchemaVersion: graph.SchemaVersionV1,
		Nodes: []graph.Node{
			{ID: "file:" + filePath, Kind: graph.NodeKindFile, Name: filePath, Path: filePath, Language: m.language},
		},
	}, []contractevents.CodeChunk{
		{ChunkID: "chunk1", SymbolName: "main", SymbolType: contractevents.SymbolTypeFunction, StartLine: 1, EndLine: 2},
	}, nil
}

type mockArtifactStore struct {
	mu        sync.Mutex
	calls     int
	artifacts map[string]graph.Artifact
	byCommit  map[string]graph.Artifact // key: repo|commit|file
	deltas    []graph.Delta
}

func newMockArtifactStore() *mockArtifactStore {
	return &mockArtifactStore{
		artifacts: make(map[string]graph.Artifact),
		byCommit:  make(map[string]graph.Artifact),
	}
}

func commitFileKey(repoID, commitSHA, filePath string) string {
	return repoID + "|" + commitSHA + "|" + filePath
}

func (m *mockArtifactStore) SaveArtifact(_ context.Context, artifact graph.Artifact) (string, string, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.calls++
	id := "art_test"
	m.artifacts[id] = artifact
	for _, node := range artifact.Nodes {
		if node.Kind == graph.NodeKindFile && node.Path != "" {
			m.byCommit[commitFileKey(artifact.RepoID, artifact.CommitSHA, node.Path)] = artifact
		}
	}
	return id, "file:///tmp/art_test.json", nil
}

func (m *mockArtifactStore) GetArtifact(_ context.Context, artifactID string) (graph.Artifact, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	artifact, ok := m.artifacts[artifactID]
	if !ok {
		return graph.Artifact{}, errors.New("not found")
	}
	return artifact, nil
}

func (m *mockArtifactStore) FindArtifactByCommitFile(_ context.Context, repoID, commitSHA, filePath string) (graph.Artifact, string, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	artifact, ok := m.byCommit[commitFileKey(repoID, commitSHA, filePath)]
	if !ok {
		return graph.Artifact{}, "", store.ErrArtifactNotFound
	}
	return artifact, "art_" + commitSHA + "_" + filePath, nil
}

func (m *mockArtifactStore) FindArtifactBySnapshotFile(_ context.Context, repoID, snapshotID, filePath string) (graph.Artifact, string, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	for key, artifact := range m.byCommit {
		parts := strings.Split(key, "|")
		if len(parts) != 3 || parts[0] != repoID || parts[2] != filePath {
			continue
		}
		if artifact.SnapshotID == snapshotID {
			return artifact, "art_" + snapshotID + "_" + filePath, nil
		}
	}
	return graph.Artifact{}, "", store.ErrArtifactNotFound
}

func (m *mockArtifactStore) SaveDelta(_ context.Context, delta graph.Delta) (string, string, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.deltas = append(m.deltas, delta)
	return "delta_test", "file:///tmp/delta_test.json", nil
}

func (m *mockArtifactStore) callCount() int {
	m.mu.Lock()
	defer m.mu.Unlock()
	return m.calls
}

type mockPublisher struct {
	mu               sync.Mutex
	chunksCalls      int
	graphCalls       int
	deltaCalls       int
	diagnosticsCalls int
	lastGraphDelta   contractevents.GraphDeltaReadyEvent
}

func (m *mockPublisher) PublishChunksReady(_ context.Context, _ contractevents.ChunksReadyEvent) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.chunksCalls++
	return nil
}

func (m *mockPublisher) PublishGraphArtifactReady(_ context.Context, _ contractevents.GraphArtifactReadyEvent) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.graphCalls++
	return nil
}

func (m *mockPublisher) PublishGraphDeltaReady(_ context.Context, event contractevents.GraphDeltaReadyEvent) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.deltaCalls++
	m.lastGraphDelta = event
	return nil
}

func (m *mockPublisher) PublishParserDiagnostics(_ context.Context, _ contractevents.ParserDiagnosticsReadyEvent) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.diagnosticsCalls++
	return nil
}

func (m *mockPublisher) counts() (chunks, graphArtifacts, deltas, diagnostics int) {
	m.mu.Lock()
	defer m.mu.Unlock()
	return m.chunksCalls, m.graphCalls, m.deltaCalls, m.diagnosticsCalls
}

func setupWorkspace(t *testing.T, files map[string]string) string {
	t.Helper()

	workspace := t.TempDir()
	repoDir := filepath.Join(workspace, "ad__example")
	if err := os.MkdirAll(repoDir, 0o755); err != nil {
		t.Fatalf("mkdir repo: %v", err)
	}

	for path, content := range files {
		fullPath := filepath.Join(repoDir, filepath.FromSlash(path))
		if err := os.MkdirAll(filepath.Dir(fullPath), 0o755); err != nil {
			t.Fatalf("mkdir parent: %v", err)
		}
		if err := os.WriteFile(fullPath, []byte(content), 0o644); err != nil {
			t.Fatalf("write file: %v", err)
		}
	}

	return workspace
}

func fileChangedEvent(snapshotID, filePath string) contractevents.FileChangedEvent {
	return contractevents.NewFileChangedEvent(
		"evt_"+snapshotID+"_"+filePath,
		"ad/example",
		snapshotID,
		"abc123",
		filePath,
		"go",
		contractevents.FileKindCode,
		contractevents.ChangeTypeAdded,
	)
}

func newTestService(t *testing.T, workspace string, analyzer *mockAnalyzer, artifactStore store.ArtifactStore, pub parser.EventPublisher) *parser.Service {
	t.Helper()

	return parser.NewService(parser.ServiceConfig{
		WorkspacePath: workspace,
		Workers:       4,
		ParserVersion: "v0.1.0-test",
	}, []parser.LanguageAnalyzer{analyzer}, pub, artifactStore)
}

func TestResolveSourcePath(t *testing.T) {
	path, err := parser.ResolveSourcePath("/data/repos", "ad/example", "internal/main.go")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	want := filepath.Join("/data/repos", "ad__example", "internal", "main.go")
	if path != want {
		t.Fatalf("expected %q, got %q", want, path)
	}
}

func TestProcessFiles_ConcurrentSuccess(t *testing.T) {
	workspace := setupWorkspace(t, map[string]string{
		"a.go": "package a\nfunc A() {}",
		"b.go": "package b\nfunc B() {}",
		"c.go": "package c\nfunc C() {}",
	})

	analyzer := &mockAnalyzer{language: "go"}
	store := newMockArtifactStore()
	pub := &mockPublisher{}
	svc := newTestService(t, workspace, analyzer, store, pub)

	events := []contractevents.FileChangedEvent{
		fileChangedEvent("snap1", "a.go"),
		fileChangedEvent("snap1", "b.go"),
		fileChangedEvent("snap1", "c.go"),
	}

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

	if analyzer.calls.Load() != 3 {
		t.Fatalf("expected 3 parse calls, got %d", analyzer.calls.Load())
	}
	if store.callCount() != 3 {
		t.Fatalf("expected 3 save calls, got %d", store.callCount())
	}
	chunks, graphs, _, diags := pub.counts()
	if chunks != 3 || graphs != 0 {
		t.Fatalf("expected 3 chunks publishes and no per-file graph, got chunks=%d graph=%d", chunks, graphs)
	}
	if diags != 0 {
		t.Fatalf("expected 0 diagnostics, got %d", diags)
	}

	for _, event := range events {
		job, ok := svc.GetParseJob(parser.JobIDFromEvent(event))
		if !ok {
			t.Fatalf("job not found for %q", event.FilePath)
		}
		if job.Status != parser.JobStatusCompleted {
			t.Fatalf("expected completed for %q, got %q", event.FilePath, job.Status)
		}
	}
}

func TestProcessFiles_OneFailureDoesNotBlock(t *testing.T) {
	workspace := setupWorkspace(t, map[string]string{
		"good.go": "package good\nfunc Good() {}",
		"bad.go":  "package bad\nfunc Bad() {}",
	})

	analyzer := &mockAnalyzer{
		language: "go",
		fn: func(_ context.Context, _ []byte, filePath string) (*graph.Artifact, []contractevents.CodeChunk, error) {
			if filePath == "bad.go" {
				return nil, nil, errors.New("parse error")
			}
			return &graph.Artifact{
				SchemaVersion: graph.SchemaVersionV1,
				Nodes:         []graph.Node{{ID: "file:" + filePath, Kind: graph.NodeKindFile, Name: filePath, Path: filePath}},
			}, []contractevents.CodeChunk{
				{ChunkID: "c1", SymbolName: "Good", SymbolType: contractevents.SymbolTypeFunction, StartLine: 1, EndLine: 2},
			}, nil
		},
	}
	store := newMockArtifactStore()
	pub := &mockPublisher{}
	svc := newTestService(t, workspace, analyzer, store, pub)

	goodEvent := fileChangedEvent("snap1", "good.go")
	badEvent := fileChangedEvent("snap1", "bad.go")

	if err := svc.ProcessFiles(context.Background(), []contractevents.FileChangedEvent{goodEvent, badEvent}); err == nil {
		t.Fatal("expected error from failed parse job")
	}

	if store.callCount() != 1 {
		t.Fatalf("expected 1 save call, got %d", store.callCount())
	}
	_, graphs, _, diags := pub.counts()
	if graphs != 0 {
		t.Fatalf("expected no per-file graph publish, got %d", graphs)
	}
	if diags != 1 {
		t.Fatalf("expected 1 diagnostics publish, got %d", diags)
	}

	goodJob, _ := svc.GetParseJob(parser.JobIDFromEvent(goodEvent))
	if goodJob.Status != parser.JobStatusCompleted {
		t.Fatalf("expected good job completed, got %q", goodJob.Status)
	}
	badJob, _ := svc.GetParseJob(parser.JobIDFromEvent(badEvent))
	if badJob.Status != parser.JobStatusFailed {
		t.Fatalf("expected bad job failed, got %q", badJob.Status)
	}
}

func TestProcessFiles_SkipsDeletedAndNonCode(t *testing.T) {
	workspace := setupWorkspace(t, map[string]string{
		"main.go": "package main\nfunc main() {}",
	})

	analyzer := &mockAnalyzer{language: "go"}
	store := newMockArtifactStore()
	pub := &mockPublisher{}
	svc := newTestService(t, workspace, analyzer, store, pub)

	deleted := fileChangedEvent("snap1", "removed.go")
	deleted.ChangeType = contractevents.ChangeTypeDeleted

	docs := fileChangedEvent("snap1", "README.md")
	docs.FileKind = contractevents.FileKindDocs
	docs.Language = "markdown"

	if err := svc.ProcessFiles(context.Background(), []contractevents.FileChangedEvent{deleted, docs}); err != nil {
		t.Fatalf("ProcessFiles: %v", err)
	}

	if analyzer.calls.Load() != 0 {
		t.Fatalf("expected 0 parse calls, got %d", analyzer.calls.Load())
	}
	if store.callCount() != 0 {
		t.Fatalf("expected 0 save calls, got %d", store.callCount())
	}

	deletedJob, _ := svc.GetParseJob(parser.JobIDFromEvent(deleted))
	if deletedJob.Status != parser.JobStatusSkipped {
		t.Fatalf("expected deleted skipped, got %q", deletedJob.Status)
	}
	docsJob, _ := svc.GetParseJob(parser.JobIDFromEvent(docs))
	if docsJob.Status != parser.JobStatusSkipped {
		t.Fatalf("expected docs skipped, got %q", docsJob.Status)
	}

	_, _, _, diags := pub.counts()
	if diags != 1 {
		t.Fatalf("expected 1 diagnostic for deleted file, got %d", diags)
	}
}

func TestProcessFiles_SkipsEmptyChunksReady(t *testing.T) {
	workspace := setupWorkspace(t, map[string]string{
		"types.go": "package types\ntype Foo struct{}",
	})

	analyzer := &mockAnalyzer{
		language: "go",
		fn: func(_ context.Context, _ []byte, filePath string) (*graph.Artifact, []contractevents.CodeChunk, error) {
			return &graph.Artifact{
				SchemaVersion: graph.SchemaVersionV1,
				Nodes:         []graph.Node{{ID: "file:" + filePath, Kind: graph.NodeKindFile, Name: filePath, Path: filePath}},
			}, nil, nil
		},
	}
	store := newMockArtifactStore()
	pub := &mockPublisher{}
	svc := newTestService(t, workspace, analyzer, store, pub)

	if err := svc.ProcessFiles(context.Background(), []contractevents.FileChangedEvent{
		fileChangedEvent("snap1", "types.go"),
	}); err != nil {
		t.Fatalf("ProcessFiles: %v", err)
	}

	chunks, graphs, _, _ := pub.counts()
	if chunks != 0 {
		t.Fatalf("expected 0 chunks.ready publishes, got %d", chunks)
	}
	if graphs != 0 {
		t.Fatalf("expected no per-file graph.artifact.ready publish, got %d", graphs)
	}
}

func TestHydrateChunkText(t *testing.T) {
	source := []byte("line1\nline2\nline3\n")
	chunks := []contractevents.CodeChunk{
		{ChunkID: "c1", StartLine: 2, EndLine: 3},
	}

	workspace := setupWorkspace(t, map[string]string{"main.go": string(source)})
	analyzer := &mockAnalyzer{
		language: "go",
		fn: func(_ context.Context, _ []byte, filePath string) (*graph.Artifact, []contractevents.CodeChunk, error) {
			return &graph.Artifact{
				SchemaVersion: graph.SchemaVersionV1,
				Nodes:         []graph.Node{{ID: "file:" + filePath, Kind: graph.NodeKindFile, Name: filePath, Path: filePath}},
			}, chunks, nil
		},
	}
	pub := &mockPublisher{}
	svc := newTestService(t, workspace, analyzer, newMockArtifactStore(), pub)

	event := fileChangedEvent("snap_hydrate", "main.go")
	if err := svc.ProcessFileChanged(context.Background(), event); err != nil {
		t.Fatalf("ProcessFileChanged: %v", err)
	}
}

func TestGetParseJob(t *testing.T) {
	workspace := setupWorkspace(t, map[string]string{
		"main.go": "package main\nfunc main() {}",
	})

	svc := newTestService(t, workspace, &mockAnalyzer{language: "go"}, newMockArtifactStore(), &mockPublisher{})

	event := fileChangedEvent("snap1", "main.go")
	if err := svc.ProcessFileChanged(context.Background(), event); err != nil {
		t.Fatalf("ProcessFileChanged: %v", err)
	}

	jobID := parser.JobIDFromEvent(event)
	job, ok := svc.GetParseJob(jobID)
	if !ok {
		t.Fatal("expected job to exist")
	}
	if job.FilePath != "main.go" {
		t.Fatalf("unexpected file path: %q", job.FilePath)
	}
	if job.Status != parser.JobStatusCompleted {
		t.Fatalf("expected completed, got %q", job.Status)
	}
	if job.ArtifactID != "art_test" {
		t.Fatalf("artifact_id: got %q, want art_test", job.ArtifactID)
	}

	diagnostics, ok := svc.GetJobDiagnostics(jobID)
	if !ok {
		t.Fatal("expected diagnostics lookup to succeed")
	}
	if len(diagnostics) != 0 {
		t.Fatalf("expected empty diagnostics for successful job, got %d", len(diagnostics))
	}
}

func TestGetJobDiagnosticsForFailedJob(t *testing.T) {
	workspace := setupWorkspace(t, map[string]string{
		"main.go": "package main\nfunc main() {}",
	})

	svc := newTestService(t, workspace, &mockAnalyzer{
		language: "go",
		fn: func(context.Context, []byte, string) (*graph.Artifact, []contractevents.CodeChunk, error) {
			return nil, nil, errors.New("parse failed")
		},
	}, newMockArtifactStore(), &mockPublisher{})

	event := fileChangedEvent("snap1", "main.go")
	if err := svc.ProcessFileChanged(context.Background(), event); err == nil {
		t.Fatal("expected ProcessFileChanged to return parse error")
	}

	jobID := parser.JobIDFromEvent(event)
	diagnostics, ok := svc.GetJobDiagnostics(jobID)
	if !ok {
		t.Fatal("expected diagnostics lookup to succeed")
	}
	if len(diagnostics) != 1 {
		t.Fatalf("expected 1 diagnostic, got %d", len(diagnostics))
	}
	if diagnostics[0].Code != "parse_failed" {
		t.Fatalf("code: got %q, want parse_failed", diagnostics[0].Code)
	}
}

func TestWorkspaceDir(t *testing.T) {
	dir, err := parser.WorkspaceDir("/data/repos", "ad/example")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	want := filepath.Join("/data/repos", "ad__example")
	if dir != want {
		t.Fatalf("expected %q, got %q", want, dir)
	}

	gatewayDir, err := parser.WorkspaceDir("/data/repos", "github:org/repo")
	if err != nil {
		t.Fatalf("gateway repo_id: %v", err)
	}
	gatewayWant := filepath.Join("/data/repos", "github__org__repo")
	if gatewayDir != gatewayWant {
		t.Fatalf("expected %q, got %q", gatewayWant, gatewayDir)
	}
}
