package gitosync

import (
	"context"
	"errors"
	"strings"
	"testing"

	"github.com/nomadcode/nomadcode-core/internal/gitoevents"
)

const milestonePath = "agent-roadmap/phase/p1/milestones/m1.md"

const milestoneMarkdownWithIdentity = `# Milestone m1

## Provider identity
- provider: plane
- tenant: acme
- project: proj-1
- work item id: wi-123
`

const milestoneMarkdownNoIdentity = `# Milestone m2

Just a body, no provider identity block.
`

// fakeRunner returns scripted stdout per (args joined) command and records the
// ordered sequence of commands it ran.
type fakeRunner struct {
	responses map[string]string
	errs      map[string]error
	calls     []string
}

func newFakeRunner() *fakeRunner {
	return &fakeRunner{responses: map[string]string{}, errs: map[string]error{}}
}

func (f *fakeRunner) Run(_ context.Context, _ string, name string, args ...string) (string, error) {
	key := name + " " + strings.Join(args, " ")
	f.calls = append(f.calls, key)
	if err, ok := f.errs[key]; ok {
		return "", err
	}
	return f.responses[key], nil
}

func newScanEvent() gitoevents.BranchUpdatedEvent {
	return gitoevents.BranchUpdatedEvent{
		RepoID: "nomadcode",
		Branch: "develop",
		Before: "aaaa",
		After:  "bbbb",
	}
}

func newTestScanner(t *testing.T, runner CommandRunner) *BranchRevisionScanner {
	t.Helper()
	s, err := NewBranchRevisionScanner(runner, ScannerConfig{
		DevelopRepoPath: "/repo",
		RemoteName:      "origin",
		Branch:          "develop",
	})
	if err != nil {
		t.Fatalf("NewBranchRevisionScanner: %v", err)
	}
	return s
}

func TestScanRunsFetchRevParseDiffShowInOrder(t *testing.T) {
	runner := newFakeRunner()
	runner.responses["git rev-parse refs/remotes/origin/develop"] = "bbbb\n"
	runner.responses["git diff --name-only aaaa..bbbb -- agent-roadmap/phase"] = milestonePath + "\n"
	runner.responses["git show bbbb:"+milestonePath] = milestoneMarkdownWithIdentity

	out, err := newTestScanner(t, runner).Scan(context.Background(), newScanEvent())
	if err != nil {
		t.Fatalf("Scan: %v", err)
	}

	wantOrder := []string{
		"git fetch --prune origin develop",
		"git rev-parse refs/remotes/origin/develop",
		"git diff --name-only aaaa..bbbb -- agent-roadmap/phase",
		"git show bbbb:" + milestonePath,
	}
	if len(runner.calls) != len(wantOrder) {
		t.Fatalf("call count: got %d (%v), want %d", len(runner.calls), runner.calls, len(wantOrder))
	}
	for i, want := range wantOrder {
		if runner.calls[i] != want {
			t.Fatalf("call[%d]: got %q, want %q", i, runner.calls[i], want)
		}
	}

	if out.Result.Revision.Revision != "bbbb" {
		t.Fatalf("revision: got %q, want bbbb", out.Result.Revision.Revision)
	}
	if len(out.Result.ScannedMilestones) != 1 || out.Result.ScannedMilestones[0].Path != milestonePath {
		t.Fatalf("scanned milestones: got %+v", out.Result.ScannedMilestones)
	}
	if got := out.Result.ScannedMilestones[0].Identity.WorkItemID; got != "wi-123" {
		t.Fatalf("identity work item id: got %q, want wi-123", got)
	}
	if len(out.Docs) != 1 || out.Docs[0].Markdown != milestoneMarkdownWithIdentity {
		t.Fatalf("docs: got %+v", out.Docs)
	}
}

func TestScanReturnsNotReadyOnRevisionMismatch(t *testing.T) {
	runner := newFakeRunner()
	// Remote develop has not caught up to the event's `after`.
	runner.responses["git rev-parse refs/remotes/origin/develop"] = "cccc\n"

	_, err := newTestScanner(t, runner).Scan(context.Background(), newScanEvent())
	if !errors.Is(err, ErrNotReady) {
		t.Fatalf("Scan: got %v, want ErrNotReady", err)
	}
	// It must not diff/show once it knows the revision is stale.
	for _, c := range runner.calls {
		if strings.HasPrefix(c, "git diff") || strings.HasPrefix(c, "git show") {
			t.Fatalf("unexpected command after mismatch: %q", c)
		}
	}
}

func TestScanDropsMilestoneWithoutProviderIdentity(t *testing.T) {
	runner := newFakeRunner()
	runner.responses["git rev-parse refs/remotes/origin/develop"] = "bbbb"
	runner.responses["git diff --name-only aaaa..bbbb -- agent-roadmap/phase"] = milestonePath
	runner.responses["git show bbbb:"+milestonePath] = milestoneMarkdownNoIdentity

	out, err := newTestScanner(t, runner).Scan(context.Background(), newScanEvent())
	if err != nil {
		t.Fatalf("Scan: %v", err)
	}
	// Kept in ChangedFiles (so the match gate's reason stays accurate) but not
	// projectable.
	if len(out.Result.ChangedFiles) != 1 {
		t.Fatalf("changed files: got %v", out.Result.ChangedFiles)
	}
	if len(out.Result.ScannedMilestones) != 0 {
		t.Fatalf("scanned milestones: want none, got %+v", out.Result.ScannedMilestones)
	}
	if len(out.Docs) != 0 {
		t.Fatalf("docs: want none, got %+v", out.Docs)
	}
}

func TestScanIgnoresNonMilestoneChangedFiles(t *testing.T) {
	runner := newFakeRunner()
	runner.responses["git rev-parse refs/remotes/origin/develop"] = "bbbb"
	runner.responses["git diff --name-only aaaa..bbbb -- agent-roadmap/phase"] =
		"agent-roadmap/phase/p1/PHASE.md\n" + milestonePath
	runner.responses["git show bbbb:"+milestonePath] = milestoneMarkdownWithIdentity

	out, err := newTestScanner(t, runner).Scan(context.Background(), newScanEvent())
	if err != nil {
		t.Fatalf("Scan: %v", err)
	}
	if len(out.Result.ChangedFiles) != 1 || out.Result.ChangedFiles[0] != milestonePath {
		t.Fatalf("changed files: got %v, want only the milestone path", out.Result.ChangedFiles)
	}
}

func TestScanUsesSingleRevisionRangeForNewBranchPush(t *testing.T) {
	runner := newFakeRunner()
	runner.responses["git rev-parse refs/remotes/origin/develop"] = "bbbb"
	runner.responses["git diff --name-only bbbb -- agent-roadmap/phase"] = ""

	ev := newScanEvent()
	ev.Before = "0000000000000000000000000000000000000000"

	if _, err := newTestScanner(t, runner).Scan(context.Background(), ev); err != nil {
		t.Fatalf("Scan: %v", err)
	}
	for _, c := range runner.calls {
		if strings.HasPrefix(c, "git diff") && !strings.Contains(c, "bbbb -- ") {
			t.Fatalf("expected single-revision diff range, got %q", c)
		}
	}
}
