package codeindex

import (
	"context"
	"encoding/json"
	"errors"
	"os"
	"os/exec"
	"path/filepath"
	"testing"
	"time"

	"github.com/google/uuid"
	"github.com/stretchr/testify/require"

	"github.com/yan1h/agent-coding-workflow/internal/infra/git"
	"github.com/yan1h/agent-coding-workflow/internal/infra/llm"
	"github.com/yan1h/agent-coding-workflow/internal/jobs"
)

// ===== fakes =====

type fakeRepo struct {
	runs      map[uuid.UUID]*Run
	replaced  map[uuid.UUID][]ChunkRow
	completed map[uuid.UUID]bool
	failed    map[uuid.UUID]string
	running   map[uuid.UUID]bool
}

func newFakeRepo() *fakeRepo {
	return &fakeRepo{
		runs: map[uuid.UUID]*Run{}, replaced: map[uuid.UUID][]ChunkRow{},
		completed: map[uuid.UUID]bool{}, failed: map[uuid.UUID]string{}, running: map[uuid.UUID]bool{},
	}
}

func (r *fakeRepo) InsertRun(_ context.Context, run *Run) (*Run, error) {
	cp := *run
	r.runs[run.ID] = &cp
	return &cp, nil
}
func (r *fakeRepo) GetRun(_ context.Context, id uuid.UUID) (*Run, error) {
	run, ok := r.runs[id]
	if !ok {
		return nil, errors.New("not found")
	}
	cp := *run
	return &cp, nil
}
func (r *fakeRepo) GetRunByCommit(_ context.Context, wsID uuid.UUID, commit, model string) (*Run, error) {
	for _, run := range r.runs {
		if run.WorkspaceID == wsID && run.CommitSHA == commit && run.EmbeddingModel == model {
			cp := *run
			return &cp, nil
		}
	}
	return nil, nil
}
func (r *fakeRepo) GetLatestCompletedRun(_ context.Context, wsID uuid.UUID) (*Run, error) {
	for _, run := range r.runs {
		if run.WorkspaceID == wsID && run.Status == StatusCompleted {
			cp := *run
			return &cp, nil
		}
	}
	return nil, nil
}
func (r *fakeRepo) MarkRunning(_ context.Context, id uuid.UUID) error {
	r.running[id] = true
	return nil
}
func (r *fakeRepo) MarkCompleted(_ context.Context, id uuid.UUID, files, chunks int) error {
	r.completed[id] = true
	if run := r.runs[id]; run != nil {
		run.Status = StatusCompleted
		run.FilesIndexed = files
		run.ChunksIndexed = chunks
	}
	return nil
}
func (r *fakeRepo) MarkFailed(_ context.Context, id uuid.UUID, msg string) error {
	r.failed[id] = msg
	if run := r.runs[id]; run != nil {
		run.Status = StatusFailed
	}
	return nil
}
func (r *fakeRepo) MarkStale(_ context.Context, _ uuid.UUID) error { return nil }
func (r *fakeRepo) MarkStuckStale(_ context.Context, _ time.Time) ([]uuid.UUID, error) {
	return nil, nil
}
func (r *fakeRepo) ReplaceChunks(_ context.Context, runID, _ uuid.UUID, _ string, rows []ChunkRow) error {
	r.replaced[runID] = rows
	return nil
}
func (r *fakeRepo) SearchKNN(context.Context, uuid.UUID, []float32, int, string, string) ([]Snippet, error) {
	return nil, nil
}

type fakeLocator struct {
	dir  string
	wsID uuid.UUID
}

func (l fakeLocator) LocateWorkspace(context.Context, uuid.UUID) (WorkspaceInfo, error) {
	return WorkspaceInfo{ID: l.wsID, MainPath: l.dir, DefaultBranch: "main"}, nil
}
func (l fakeLocator) LocateWorktree(context.Context, uuid.UUID) (string, string, uuid.UUID, error) {
	return l.dir, "main", l.wsID, nil
}

type fakeEmbedSel struct {
	emb llm.Embedder
	err error
}

func (s fakeEmbedSel) Embedder(context.Context) (llm.Embedder, uuid.UUID, string, int, error) {
	if s.err != nil {
		return nil, uuid.Nil, "", 0, s.err
	}
	return s.emb, uuid.New(), "fake-model", llm.PlatformEmbeddingDims, nil
}

func mustGit(t *testing.T, dir string, args ...string) {
	t.Helper()
	c := exec.Command("git", args...)
	c.Dir = dir
	out, err := c.CombinedOutput()
	require.NoError(t, err, "git %v: %s", args, string(out))
}

func tempGitRepo(t *testing.T) (dir, sha string) {
	t.Helper()
	if _, err := exec.LookPath("git"); err != nil {
		t.Skip("git not in PATH")
	}
	dir = t.TempDir()
	mustGit(t, dir, "init", "-b", "main")
	mustGit(t, dir, "config", "user.email", "t@e.com")
	mustGit(t, dir, "config", "user.name", "T")
	require.NoError(t, os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\n\nfunc main() {}\n"), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(dir, "util.go"), []byte("package main\n\nfunc add(a, b int) int { return a + b }\n"), 0o644))
	mustGit(t, dir, "add", ".")
	mustGit(t, dir, "commit", "-m", "init")
	out, err := exec.Command("git", "-C", dir, "rev-parse", "HEAD").Output()
	require.NoError(t, err)
	sha = string(out[:len(out)-1])
	return dir, sha
}

func TestBuildRunner_Success(t *testing.T) {
	dir, sha := tempGitRepo(t)
	wsID := uuid.New()
	repo := newFakeRepo()
	run := &Run{
		ID: uuid.New(), WorkspaceID: wsID, Branch: "main", CommitSHA: sha,
		Status: StatusPending, EmbeddingModel: "fake-model", Dims: llm.PlatformEmbeddingDims,
	}
	repo.runs[run.ID] = run

	gitr := git.NewDefaultRunner(git.DefaultConfig())
	br := NewBuildRunner(repo, fakeLocator{dir: dir, wsID: wsID}, gitr,
		fakeEmbedSel{emb: llm.NewFakeEmbedder()}, NewChunker(), nil, nil)

	payload, _ := json.Marshal(buildPayload{RunID: run.ID})
	err := br.Handle(context.Background(), jobs.Job{Payload: payload})
	require.NoError(t, err)
	require.True(t, repo.completed[run.ID])
	require.True(t, repo.running[run.ID])
	require.NotEmpty(t, repo.replaced[run.ID], "chunks must be inserted")
	// Each inserted row must carry an embedding of platform dims.
	for _, row := range repo.replaced[run.ID] {
		require.Len(t, row.Embedding, llm.PlatformEmbeddingDims)
	}
}

func TestBuildRunner_FailureMarksFailedAndNotifies(t *testing.T) {
	dir, sha := tempGitRepo(t)
	wsID := uuid.New()
	repo := newFakeRepo()
	run := &Run{
		ID: uuid.New(), WorkspaceID: wsID, Branch: "main", CommitSHA: sha,
		Status: StatusPending, EmbeddingModel: "fake-model", Dims: llm.PlatformEmbeddingDims,
	}
	repo.runs[run.ID] = run

	embErr := errors.New("embed boom")
	notified := false
	gitr := git.NewDefaultRunner(git.DefaultConfig())
	br := NewBuildRunner(repo, fakeLocator{dir: dir, wsID: wsID}, gitr,
		fakeEmbedSel{emb: &llm.FakeEmbedder{Dims: llm.PlatformEmbeddingDims, Err: embErr}},
		NewChunker(),
		func(context.Context, *Run, error) { notified = true }, nil)

	payload, _ := json.Marshal(buildPayload{RunID: run.ID})
	err := br.Handle(context.Background(), jobs.Job{Payload: payload})
	require.Error(t, err)
	require.Contains(t, repo.failed[run.ID], "embed boom")
	require.True(t, notified)
}

func TestBuildRunner_SkipsAlreadyCompleted(t *testing.T) {
	repo := newFakeRepo()
	run := &Run{ID: uuid.New(), Status: StatusCompleted, EmbeddingModel: "fake-model", Dims: llm.PlatformEmbeddingDims}
	repo.runs[run.ID] = run
	br := NewBuildRunner(repo, fakeLocator{}, nil, fakeEmbedSel{emb: llm.NewFakeEmbedder()}, NewChunker(), nil, nil)
	payload, _ := json.Marshal(buildPayload{RunID: run.ID})
	require.NoError(t, br.Handle(context.Background(), jobs.Job{Payload: payload}))
	require.Empty(t, repo.replaced[run.ID])
}

var _ Repository = (*fakeRepo)(nil)
