package gitosync

import (
	"context"
	"errors"
	"testing"

	"github.com/nomadcode/nomadcode-core/internal/gitoevents"
	"github.com/nomadcode/nomadcode-core/internal/roadmapsync"
	"github.com/nomadcode/nomadcode-core/internal/scheduler"
	"github.com/nomadcode/nomadcode-core/internal/workitem"
)

type fakeScanner struct {
	out ScanOutput
	err error
}

func (f fakeScanner) Scan(context.Context, gitoevents.BranchUpdatedEvent) (ScanOutput, error) {
	return f.out, f.err
}

type fakeReader struct {
	item workitem.WorkItem
	err  error
	refs []workitem.Ref
}

func (f *fakeReader) FetchWorkItem(_ context.Context, ref workitem.Ref) (workitem.WorkItem, error) {
	f.refs = append(f.refs, ref)
	if f.err != nil {
		return workitem.WorkItem{}, f.err
	}
	return f.item, nil
}

type fakeEnqueuer struct {
	jobs []scheduler.RoadmapCreationSyncJobArgs
	err  error
}

func (f *fakeEnqueuer) EnqueueRoadmapCreationSync(_ context.Context, args scheduler.RoadmapCreationSyncJobArgs) error {
	if f.err != nil {
		return f.err
	}
	f.jobs = append(f.jobs, args)
	return nil
}

func matchedIdentity() roadmapsync.Identity {
	id, err := roadmapsync.Identity{
		Shape:                roadmapsync.ShapeMilestone,
		RoadmapMilestonePath: milestonePath,
		Provider:             "plane",
		Tenant:               "acme",
		Project:              "proj-1",
		WorkItemID:           "wi-123",
	}.Normalize()
	if err != nil {
		panic(err)
	}
	return id
}

func matchedScanOutput() ScanOutput {
	identity := matchedIdentity()
	scanned := roadmapsync.ScannedMilestone{Path: milestonePath, Identity: identity}
	return ScanOutput{
		Result: roadmapsync.ScanResult{
			Revision:          roadmapsync.Revision{Branch: "develop", Revision: "bbbb"},
			ChangedFiles:      []string{milestonePath},
			ScannedMilestones: []roadmapsync.ScannedMilestone{scanned},
		},
		Docs: []ScannedMilestoneDoc{{Milestone: scanned, Markdown: milestoneMarkdownWithIdentity}},
	}
}

func newTestBridge(t *testing.T, scanner Scanner, reader workitem.Reader, enq Enqueuer) *Bridge {
	t.Helper()
	b, err := NewBridge(scanner, reader, enq, NewInMemoryRevisionStore(), BridgeConfig{
		RepoID:      "nomadcode",
		Branch:      "develop",
		TodoStateID: "state-todo",
	}, nil)
	if err != nil {
		t.Fatalf("NewBridge: %v", err)
	}
	return b
}

func TestBridgeEnqueuesExactJobArgs(t *testing.T) {
	reader := &fakeReader{item: workitem.WorkItem{
		Title:           "WI",
		DescriptionHTML: "<p>original html</p>",
		DescriptionText: "original text",
		State:           "state-backlog",
	}}
	enq := &fakeEnqueuer{}
	bridge := newTestBridge(t, fakeScanner{out: matchedScanOutput()}, reader, enq)

	if err := bridge.Handle(context.Background(), newScanEvent()); err != nil {
		t.Fatalf("Handle: %v", err)
	}

	if len(enq.jobs) != 1 {
		t.Fatalf("jobs: got %d, want 1", len(enq.jobs))
	}
	job := enq.jobs[0]
	if job.Expected.WorkItemID != "wi-123" {
		t.Fatalf("Expected.WorkItemID: got %q", job.Expected.WorkItemID)
	}
	if job.Ref.Provider != "plane" || job.Ref.ID != "wi-123" || job.Ref.Project != "proj-1" {
		t.Fatalf("Ref: got %+v", job.Ref)
	}
	if job.OriginalBody != "<p>original html</p>" {
		t.Fatalf("OriginalBody: got %q, want HTML preferred", job.OriginalBody)
	}
	if job.TodoStateID != "state-todo" {
		t.Fatalf("TodoStateID: got %q", job.TodoStateID)
	}
	if job.MilestoneMarkdown != milestoneMarkdownWithIdentity {
		t.Fatalf("MilestoneMarkdown mismatch")
	}
	if job.RoadmapRevision != "bbbb" {
		t.Fatalf("RoadmapRevision: got %q", job.RoadmapRevision)
	}
	// Plane State is mutable and the sync moves it to Todo; it must never become
	// the ProviderRevision, or a same-develop-revision reprocess would conflict
	// before the step ledger is consulted.
	if job.ProviderRevision != "" {
		t.Fatalf("ProviderRevision: got %q, want empty (mutable Plane state must not be a revision)", job.ProviderRevision)
	}
	if job.ExternalSource != "nomadcode" || job.ExternalID != "wi-123" {
		t.Fatalf("External tags: got source=%q id=%q", job.ExternalSource, job.ExternalID)
	}
}

// TestBridgeSameRoadmapRevisionReprocessDoesNotConflictOnProviderState pins the
// reconcile contract this review targets: with ProviderRevision left empty, a
// re-process of the SAME develop revision after the Plane state has moved (e.g.
// backlog -> todo) reconciles cleanly (complete/resume), while a DIFFERENT
// roadmap revision is still guarded by RoadmapRevision.
func TestBridgeSameRoadmapRevisionReprocessDoesNotConflictOnProviderState(t *testing.T) {
	identity := matchedIdentity()
	// Persisted identity from a first cycle: same develop revision, no provider
	// revision recorded, and the Plane state already moved to todo since.
	existing := roadmapsync.ExistingIdentity{
		Found:            true,
		Identity:         identity,
		ProviderRevision: "",
		RoadmapRevision:  "bbbb",
	}

	// Trigger args as the bridge now builds them: empty ProviderRevision, develop
	// revision in RoadmapRevision.
	sameRevision := roadmapsync.ReconcileCreationCycle(roadmapsync.ReconcileInput{
		Identity:         identity,
		ProviderRevision: "",
		RoadmapRevision:  "bbbb",
		Existing:         existing,
		CompletedSteps:   map[roadmapsync.Step]bool{},
	})
	if sameRevision.Action == roadmapsync.ActionConflict {
		t.Fatalf("same develop revision reprocess must not conflict, got %q (%s)",
			sameRevision.Action, sameRevision.Reason)
	}

	diffRevision := roadmapsync.ReconcileCreationCycle(roadmapsync.ReconcileInput{
		Identity:         identity,
		ProviderRevision: "",
		RoadmapRevision:  "cccc",
		Existing:         existing,
		CompletedSteps:   map[roadmapsync.Step]bool{},
	})
	if diffRevision.Action != roadmapsync.ActionConflict {
		t.Fatalf("different develop revision reprocess must conflict via RoadmapRevision guard, got %q",
			diffRevision.Action)
	}
}

func TestBridgeOriginalBodyFallsBackToText(t *testing.T) {
	reader := &fakeReader{item: workitem.WorkItem{DescriptionText: "only text"}}
	enq := &fakeEnqueuer{}
	bridge := newTestBridge(t, fakeScanner{out: matchedScanOutput()}, reader, enq)

	if err := bridge.Handle(context.Background(), newScanEvent()); err != nil {
		t.Fatalf("Handle: %v", err)
	}
	if enq.jobs[0].OriginalBody != "only text" {
		t.Fatalf("OriginalBody fallback: got %q", enq.jobs[0].OriginalBody)
	}
}

func TestBridgeIgnoresWrongRepoBranch(t *testing.T) {
	enq := &fakeEnqueuer{}
	bridge := newTestBridge(t, fakeScanner{out: matchedScanOutput()}, &fakeReader{}, enq)

	ev := newScanEvent()
	ev.RepoID = "other-repo"

	if err := bridge.Handle(context.Background(), ev); err != nil {
		t.Fatalf("Handle: %v", err)
	}
	if len(enq.jobs) != 0 {
		t.Fatalf("wrong repo: want no enqueue, got %d", len(enq.jobs))
	}
}

func TestBridgeDeduplicatesSameRevision(t *testing.T) {
	enq := &fakeEnqueuer{}
	reader := &fakeReader{item: workitem.WorkItem{DescriptionText: "body"}}
	bridge := newTestBridge(t, fakeScanner{out: matchedScanOutput()}, reader, enq)

	ev := newScanEvent()
	if err := bridge.Handle(context.Background(), ev); err != nil {
		t.Fatalf("Handle first: %v", err)
	}
	if err := bridge.Handle(context.Background(), ev); err != nil {
		t.Fatalf("Handle second: %v", err)
	}
	if len(enq.jobs) != 1 {
		t.Fatalf("dedup: want 1 enqueue across two identical events, got %d", len(enq.jobs))
	}
}

func TestBridgeNotReadyDoesNotMarkProcessed(t *testing.T) {
	enq := &fakeEnqueuer{}
	store := NewInMemoryRevisionStore()
	b, err := NewBridge(fakeScanner{err: ErrNotReady}, &fakeReader{}, enq, store, BridgeConfig{
		RepoID:      "nomadcode",
		Branch:      "develop",
		TodoStateID: "state-todo",
	}, nil)
	if err != nil {
		t.Fatalf("NewBridge: %v", err)
	}

	ev := newScanEvent()
	if err := b.Handle(context.Background(), ev); !errors.Is(err, ErrNotReady) {
		t.Fatalf("Handle: got %v, want ErrNotReady", err)
	}
	if store.Seen(ev.RepoID, ev.Branch, ev.After) {
		t.Fatal("not-ready revision must not be marked processed")
	}
}

func TestBridgeFetchErrorIsSurfaced(t *testing.T) {
	enq := &fakeEnqueuer{}
	reader := &fakeReader{err: errors.New("fetch boom")}
	bridge := newTestBridge(t, fakeScanner{out: matchedScanOutput()}, reader, enq)

	if err := bridge.Handle(context.Background(), newScanEvent()); err == nil {
		t.Fatal("Handle: want fetch error surfaced, got nil")
	}
	if len(enq.jobs) != 0 {
		t.Fatalf("want no enqueue on fetch error, got %d", len(enq.jobs))
	}
}

func TestNewBridgeRequiresTodoStateID(t *testing.T) {
	_, err := NewBridge(fakeScanner{}, &fakeReader{}, &fakeEnqueuer{}, nil, BridgeConfig{
		RepoID: "nomadcode",
	}, nil)
	if err == nil {
		t.Fatal("NewBridge: want error when todo state id is empty")
	}
}
