package service_test

import (
	"context"
	"errors"
	"io"
	"log/slog"
	"sync"
	"testing"
	"time"

	gitcontract "bit.admedia.com/scm/ad/adpilot-indexing-repo-sync.com/internal/contracts/git"
	"bit.admedia.com/scm/ad/adpilot-indexing-repo-sync.com/internal/contracts/events"
	"bit.admedia.com/scm/ad/adpilot-indexing-repo-sync.com/internal/domain"
	"bit.admedia.com/scm/ad/adpilot-indexing-repo-sync.com/internal/service"
)

type mockGitProvider struct {
	result gitcontract.SyncResult
	err    error
	called bool
	block  chan struct{}
	done   chan struct{}
}

func (m *mockGitProvider) SyncRepository(ctx context.Context, repoID, ref string) (gitcontract.SyncResult, error) {
	m.called = true
	if m.block != nil {
		close(m.done)
		select {
		case <-m.block:
		case <-ctx.Done():
			return gitcontract.SyncResult{}, ctx.Err()
		}
	}
	if m.err != nil {
		return gitcontract.SyncResult{}, m.err
	}
	return m.result, nil
}

func (m *mockGitProvider) SyncRepositoryWithAuth(ctx context.Context, opts gitcontract.SyncAuthOptions) (gitcontract.SyncResult, error) {
	return m.SyncRepository(ctx, opts.RepoID, opts.Ref)
}

type mockEventPublisher struct {
	mu               sync.Mutex
	snapshotCalls    int
	fileCalls        int
	commitCalls      int
	failOnSnapshot   bool
	failOnFileIndex  int
	failOnCommitIndex int
	err              error
	order            []string
}

func (m *mockEventPublisher) PublishRepoSnapshotReady(_ context.Context, _ events.RepoSnapshotReadyEvent) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.snapshotCalls++
	m.order = append(m.order, "snapshot")
	if m.failOnSnapshot {
		if m.err != nil {
			return m.err
		}
		return errors.New("snapshot publish failed")
	}
	return nil
}

func (m *mockEventPublisher) PublishFileChanged(_ context.Context, _ events.FileChangedEvent) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	idx := m.fileCalls
	m.fileCalls++
	m.order = append(m.order, "file")
	if m.failOnFileIndex >= 0 && idx == m.failOnFileIndex {
		if m.err != nil {
			return m.err
		}
		return errors.New("file publish failed")
	}
	return nil
}

func (m *mockEventPublisher) PublishCommitsChanged(_ context.Context, _ events.CommitsChangedEvent) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	idx := m.commitCalls
	m.commitCalls++
	m.order = append(m.order, "commit")
	if m.failOnCommitIndex >= 0 && idx == m.failOnCommitIndex {
		if m.err != nil {
			return m.err
		}
		return errors.New("commit publish failed")
	}
	return nil
}

func testLogger() *slog.Logger {
	return slog.New(slog.NewTextHandler(io.Discard, nil))
}

func sampleSyncResult() gitcontract.SyncResult {
	return gitcontract.SyncResult{
		CloneURL: "https://bit.admedia.com/scm/ad/example-repo.git",
		Snapshot: gitcontract.RepositorySnapshot{
			RepoID:     "ad/example-repo",
			SnapshotID: "snap_test12345678",
			CommitSHA:  "abc123",
			Ref:        "main",
			Status:     gitcontract.SnapshotStatusReady,
		},
		FileChanges: []gitcontract.FileChange{
			{
				Path:       "main.go",
				ChangeType: events.ChangeTypeAdded,
				Language:   "go",
				FileKind:   events.FileKindCode,
			},
			{
				Path:       "docs/readme.md",
				ChangeType: events.ChangeTypeAdded,
				Language:   "markdown",
				FileKind:   events.FileKindDocs,
			},
		},
		CommitChanges: []gitcontract.CommitChange{
			{CommitSHA: "aaa111", ParentSHA: ""},
			{CommitSHA: "bbb222", ParentSHA: "aaa111"},
		},
		IsFirstSync: true,
	}
}

func newTestService(git *mockGitProvider, pub *mockEventPublisher) *service.SyncService {
	return service.NewSyncService(git, pub, newMockMetadataStore(), testLogger())
}

func TestSyncRepository_Success(t *testing.T) {
	git := &mockGitProvider{result: sampleSyncResult()}
	pub := &mockEventPublisher{failOnFileIndex: -1, failOnCommitIndex: -1}
	store := newMockMetadataStore()
	svc := service.NewSyncService(git, pub, store, testLogger())

	if err := svc.SyncRepository(context.Background(), "ad/example-repo", "main"); err != nil {
		t.Fatalf("SyncRepository: %v", err)
	}
	if !git.called {
		t.Fatal("expected git provider to be called")
	}
	if pub.snapshotCalls != 1 {
		t.Fatalf("snapshot calls: got %d, want 1", pub.snapshotCalls)
	}
	if pub.fileCalls != 2 {
		t.Fatalf("file calls: got %d, want 2", pub.fileCalls)
	}
	if pub.commitCalls != 2 {
		t.Fatalf("commit calls: got %d, want 2", pub.commitCalls)
	}
	if store.completedIndexingRuns() != 1 {
		t.Fatalf("completed indexing runs: got %d, want 1", store.completedIndexingRuns())
	}
	if store.snapshotFileCount() != 2 {
		t.Fatalf("snapshot files: got %d, want 2", store.snapshotFileCount())
	}
	if svc.Status() != service.StatusIdle {
		t.Fatalf("status: got %q, want idle", svc.Status())
	}
	if store.completedJobs() != 1 {
		t.Fatalf("completed jobs: got %d, want 1", store.completedJobs())
	}
	if store.repositoryCount() != 1 {
		t.Fatalf("repositories saved: got %d, want 1", store.repositoryCount())
	}
	if store.snapshotCount() != 1 {
		t.Fatalf("snapshots saved: got %d, want 1", store.snapshotCount())
	}

	snapshot, ok := svc.GetSnapshot("ad/example-repo", "snap_test12345678")
	if !ok {
		t.Fatal("expected cached snapshot")
	}
	if snapshot.CommitSHA != "abc123" {
		t.Fatalf("commit sha: got %q", snapshot.CommitSHA)
	}
	latest, ok := svc.GetLatestSnapshot("ad/example-repo")
	if !ok || latest.SnapshotID != "snap_test12345678" {
		t.Fatalf("unexpected latest snapshot: %+v ok=%v", latest, ok)
	}
}

func TestSyncRepository_PublishOrder(t *testing.T) {
	git := &mockGitProvider{result: sampleSyncResult()}
	pub := &mockEventPublisher{failOnFileIndex: -1, failOnCommitIndex: -1}
	svc := newTestService(git, pub)

	if err := svc.SyncRepository(context.Background(), "ad/example-repo", "main"); err != nil {
		t.Fatalf("SyncRepository: %v", err)
	}

	want := []string{"snapshot", "file", "file", "commit", "commit"}
	pub.mu.Lock()
	got := append([]string(nil), pub.order...)
	pub.mu.Unlock()
	if len(got) != len(want) {
		t.Fatalf("order len: got %v, want %v", got, want)
	}
	for i := range want {
		if got[i] != want[i] {
			t.Fatalf("order[%d]: got %q, want %q (full: %v)", i, got[i], want[i], got)
		}
	}
}

func TestSyncRepository_GitError(t *testing.T) {
	git := &mockGitProvider{err: errors.New("clone failed")}
	pub := &mockEventPublisher{failOnFileIndex: -1, failOnCommitIndex: -1}
	store := newMockMetadataStore()
	svc := service.NewSyncService(git, pub, store, testLogger())

	err := svc.SyncRepository(context.Background(), "ad/example-repo", "main")
	if err == nil {
		t.Fatal("expected error")
	}
	if pub.snapshotCalls != 0 || pub.fileCalls != 0 {
		t.Fatalf("expected no publishes, snapshot=%d file=%d", pub.snapshotCalls, pub.fileCalls)
	}
	if store.snapshotCount() != 0 {
		t.Fatalf("expected no snapshots saved, got %d", store.snapshotCount())
	}
	if store.failedJobs() != 1 {
		t.Fatalf("failed jobs: got %d, want 1", store.failedJobs())
	}
	if svc.Status() != service.StatusError {
		t.Fatalf("status: got %q, want error", svc.Status())
	}
}

func TestSyncRepository_PublishSnapshotError(t *testing.T) {
	git := &mockGitProvider{result: sampleSyncResult()}
	pub := &mockEventPublisher{failOnSnapshot: true, failOnFileIndex: -1, failOnCommitIndex: -1}
	store := newMockMetadataStore()
	svc := service.NewSyncService(git, pub, store, testLogger())

	err := svc.SyncRepository(context.Background(), "ad/example-repo", "main")
	if err == nil {
		t.Fatal("expected error")
	}
	if pub.snapshotCalls != 1 {
		t.Fatalf("snapshot calls: got %d, want 1", pub.snapshotCalls)
	}
	if pub.fileCalls != 0 {
		t.Fatalf("file calls: got %d, want 0", pub.fileCalls)
	}
	if store.indexingRunCount() != 1 {
		t.Fatalf("indexing runs: got %d, want 1", store.indexingRunCount())
	}
	if store.completedIndexingRuns() != 0 {
		t.Fatalf("expected indexing run not completed on publish failure")
	}
	if svc.Status() != service.StatusError {
		t.Fatalf("status: got %q, want error", svc.Status())
	}
}

func TestSyncRepository_PublishFileError(t *testing.T) {
	git := &mockGitProvider{result: sampleSyncResult()}
	pub := &mockEventPublisher{failOnFileIndex: 1, failOnSnapshot: false, failOnCommitIndex: -1}
	store := newMockMetadataStore()
	svc := service.NewSyncService(git, pub, store, testLogger())

	err := svc.SyncRepository(context.Background(), "ad/example-repo", "main")
	if err == nil {
		t.Fatal("expected error")
	}
	if pub.snapshotCalls != 1 {
		t.Fatalf("snapshot calls: got %d, want 1", pub.snapshotCalls)
	}
	if pub.fileCalls != 2 {
		t.Fatalf("file calls: got %d, want 2", pub.fileCalls)
	}
	if pub.commitCalls != 0 {
		t.Fatalf("commit calls: got %d, want 0", pub.commitCalls)
	}
	if store.completedIndexingRuns() != 0 {
		t.Fatalf("expected indexing run not completed on publish failure")
	}
	if svc.Status() != service.StatusError {
		t.Fatalf("status: got %q, want error", svc.Status())
	}
}

func TestSyncRepository_IndexingRunExpectedCounts(t *testing.T) {
	git := &mockGitProvider{result: sampleSyncResult()}
	pub := &mockEventPublisher{failOnFileIndex: -1, failOnCommitIndex: -1}
	store := newMockMetadataStore()
	svc := service.NewSyncService(git, pub, store, testLogger())

	if err := svc.SyncRepository(context.Background(), "ad/example-repo", "main"); err != nil {
		t.Fatalf("SyncRepository: %v", err)
	}

	run, ok, err := store.GetIndexingRunBySnapshot(context.Background(), "ad/example-repo", "snap_test12345678")
	if err != nil {
		t.Fatalf("GetIndexingRunBySnapshot: %v", err)
	}
	if !ok {
		t.Fatal("expected indexing run")
	}
	if run.Mode != domain.IndexingRunModeFresh {
		t.Fatalf("mode: got %q, want fresh", run.Mode)
	}
	if run.ExpectedCodeFiles != 1 {
		t.Fatalf("expected code files: got %d, want 1", run.ExpectedCodeFiles)
	}
	if run.ExpectedDocsFiles != 1 {
		t.Fatalf("expected docs files: got %d, want 1", run.ExpectedDocsFiles)
	}
	if run.ExpectedCommits != 2 {
		t.Fatalf("expected commits: got %d, want 2", run.ExpectedCommits)
	}
	if run.Status != domain.IndexingRunStatusCompleted {
		t.Fatalf("status: got %q, want completed", run.Status)
	}
	if run.Stages.RepoSync != domain.StageStatusCompleted {
		t.Fatalf("repo_sync stage: got %q", run.Stages.RepoSync)
	}
}

func TestSyncRepository_PublishCommitError(t *testing.T) {
	git := &mockGitProvider{result: sampleSyncResult()}
	pub := &mockEventPublisher{failOnFileIndex: -1, failOnCommitIndex: 0}
	store := newMockMetadataStore()
	svc := service.NewSyncService(git, pub, store, testLogger())

	err := svc.SyncRepository(context.Background(), "ad/example-repo", "main")
	if err == nil {
		t.Fatal("expected error")
	}
	if pub.commitCalls != 1 {
		t.Fatalf("commit calls: got %d, want 1", pub.commitCalls)
	}
	run, ok, err := store.GetIndexingRunBySnapshot(context.Background(), "ad/example-repo", "snap_test12345678")
	if err != nil || !ok {
		t.Fatalf("GetIndexingRunBySnapshot: ok=%v err=%v", ok, err)
	}
	if run.Status != domain.IndexingRunStatusFailed {
		t.Fatalf("indexing run status: got %q, want failed", run.Status)
	}
}

func TestSyncRepository_ContextCanceled(t *testing.T) {
	git := &mockGitProvider{
		block: make(chan struct{}),
		done:  make(chan struct{}),
	}
	pub := &mockEventPublisher{failOnFileIndex: -1, failOnCommitIndex: -1}
	svc := newTestService(git, pub)

	ctx, cancel := context.WithCancel(context.Background())
	errCh := make(chan error, 1)
	go func() {
		errCh <- svc.SyncRepository(ctx, "ad/example-repo", "main")
	}()

	select {
	case <-git.done:
	case <-time.After(time.Second):
		t.Fatal("git sync did not start")
	}
	cancel()

	select {
	case err := <-errCh:
		if !errors.Is(err, context.Canceled) {
			t.Fatalf("expected context canceled, got %v", err)
		}
	case <-time.After(time.Second):
		t.Fatal("sync did not return after cancel")
	}
	close(git.block)
}

func TestSyncRepository_FirstSyncAllAdded(t *testing.T) {
	result := sampleSyncResult()
	git := &mockGitProvider{result: result}
	pub := &mockEventPublisher{failOnFileIndex: -1, failOnCommitIndex: -1}
	svc := newTestService(git, pub)

	if err := svc.SyncRepository(context.Background(), "ad/example-repo", "main"); err != nil {
		t.Fatalf("SyncRepository: %v", err)
	}
	if !result.IsFirstSync {
		t.Fatal("fixture should be first sync")
	}
	if pub.fileCalls != len(result.FileChanges) {
		t.Fatalf("file calls: got %d, want %d", pub.fileCalls, len(result.FileChanges))
	}
}

func TestGetSnapshot_NotFound(t *testing.T) {
	svc := service.NewSyncService(&mockGitProvider{}, &mockEventPublisher{failOnFileIndex: -1, failOnCommitIndex: -1}, newMockMetadataStore(), testLogger())
	_, ok := svc.GetSnapshot("ad/missing", "snap_missing")
	if ok {
		t.Fatal("expected snapshot not found")
	}
}

func TestStatusTransitions(t *testing.T) {
	git := &mockGitProvider{
		result: sampleSyncResult(),
		block:  make(chan struct{}),
		done:   make(chan struct{}),
	}
	pub := &mockEventPublisher{failOnFileIndex: -1, failOnCommitIndex: -1}
	svc := newTestService(git, pub)

	if svc.Status() != service.StatusIdle {
		t.Fatalf("initial status: got %q", svc.Status())
	}

	errCh := make(chan error, 1)
	go func() {
		errCh <- svc.SyncRepository(context.Background(), "ad/example-repo", "main")
	}()

	select {
	case <-git.done:
	case <-time.After(time.Second):
		t.Fatal("git sync did not start")
	}
	if svc.Status() != service.StatusSyncing {
		t.Fatalf("during sync status: got %q, want syncing", svc.Status())
	}

	close(git.block)

	select {
	case err := <-errCh:
		if err != nil {
			t.Fatalf("SyncRepository: %v", err)
		}
	case <-time.After(time.Second):
		t.Fatal("sync did not complete")
	}
	if svc.Status() != service.StatusIdle {
		t.Fatalf("after success status: got %q, want idle", svc.Status())
	}
}

func TestTriggerSync_AcceptsAndCompletes(t *testing.T) {
	git := &mockGitProvider{result: sampleSyncResult()}
	pub := &mockEventPublisher{failOnFileIndex: -1, failOnCommitIndex: -1}
	store := newMockMetadataStore()
	svc := service.NewSyncService(git, pub, store, testLogger())

	jobID, err := svc.TriggerSync("ad/example-repo", "")
	if err != nil {
		t.Fatalf("TriggerSync: %v", err)
	}
	if jobID == "" {
		t.Fatal("expected job id")
	}

	deadline := time.Now().Add(2 * time.Second)
	for time.Now().Before(deadline) {
		if store.completedJobs() == 1 {
			break
		}
		time.Sleep(10 * time.Millisecond)
	}
	if store.completedJobs() != 1 {
		t.Fatalf("completed jobs: got %d, want 1", store.completedJobs())
	}
	if !git.called {
		t.Fatal("expected git provider to be called")
	}
}

func TestWaitForSyncs(t *testing.T) {
	git := &mockGitProvider{
		result: sampleSyncResult(),
		block:  make(chan struct{}),
		done:   make(chan struct{}),
	}
	svc := newTestService(git, &mockEventPublisher{failOnFileIndex: -1, failOnCommitIndex: -1})

	go func() {
		_ = svc.SyncRepository(context.Background(), "ad/example-repo", "main")
	}()

	select {
	case <-git.done:
	case <-time.After(time.Second):
		t.Fatal("sync did not start")
	}

	waitDone := make(chan struct{})
	go func() {
		svc.WaitForSyncs(context.Background())
		close(waitDone)
	}()

	select {
	case <-waitDone:
		t.Fatal("WaitForSyncs returned before sync completed")
	case <-time.After(50 * time.Millisecond):
	}

	close(git.block)

	select {
	case <-waitDone:
	case <-time.After(time.Second):
		t.Fatal("WaitForSyncs did not return after sync completed")
	}
}

func TestWaitForSyncsRespectsContext(t *testing.T) {
	git := &mockGitProvider{
		result: sampleSyncResult(),
		block:  make(chan struct{}),
		done:   make(chan struct{}),
	}
	svc := newTestService(git, &mockEventPublisher{failOnFileIndex: -1, failOnCommitIndex: -1})

	go func() {
		_ = svc.SyncRepository(context.Background(), "ad/example-repo", "main")
	}()

	select {
	case <-git.done:
	case <-time.After(time.Second):
		t.Fatal("sync did not start")
	}

	ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
	defer cancel()
	svc.WaitForSyncs(ctx)

	if svc.ActiveSyncCount() != 1 {
		t.Fatalf("expected sync still running, active=%d", svc.ActiveSyncCount())
	}

	close(git.block)
	deadline := time.Now().Add(time.Second)
	for time.Now().Before(deadline) && svc.ActiveSyncCount() > 0 {
		time.Sleep(10 * time.Millisecond)
	}
}

func TestSyncRepository_NoOpDeltaSkipsIndexingPipeline(t *testing.T) {
	store := newMockMetadataStore()
	existingSnapshot := domain.Snapshot{
		SnapshotID: "snap_existing",
		RepoID:     "ad/example-repo",
		CommitSHA:  "abc123",
		Ref:        "main",
		FileCount:  2,
		Status:     gitcontract.SnapshotStatusReady,
		CreatedAt:  time.Now().UTC(),
	}
	if err := store.SaveSnapshot(context.Background(), existingSnapshot); err != nil {
		t.Fatalf("seed snapshot: %v", err)
	}

	noOpResult := sampleSyncResult()
	noOpResult.IsFirstSync = false
	noOpResult.Snapshot.SnapshotID = "snap_noop"
	noOpResult.FileChanges = nil
	noOpResult.CommitChanges = nil

	git := &mockGitProvider{result: noOpResult}
	pub := &mockEventPublisher{failOnFileIndex: -1, failOnCommitIndex: -1}
	svc := service.NewSyncService(git, pub, store, testLogger())

	if err := svc.SyncRepository(context.Background(), "ad/example-repo", "main"); err != nil {
		t.Fatalf("SyncRepository: %v", err)
	}
	if pub.snapshotCalls != 0 || pub.fileCalls != 0 || pub.commitCalls != 0 {
		t.Fatalf("expected no publishes on no-op sync, snapshot=%d file=%d commit=%d",
			pub.snapshotCalls, pub.fileCalls, pub.commitCalls)
	}
	if store.indexingRunCount() != 0 {
		t.Fatalf("expected no indexing run, got %d", store.indexingRunCount())
	}
	if store.snapshotCount() != 1 {
		t.Fatalf("expected existing snapshot only, got %d", store.snapshotCount())
	}
	latest, ok := svc.GetLatestSnapshot("ad/example-repo")
	if !ok || latest.SnapshotID != "snap_existing" {
		t.Fatalf("latest snapshot: got %+v ok=%v", latest, ok)
	}
}
