package scheduler

import (
	"context"
	"encoding/json"
	"errors"
	"testing"

	"github.com/jackc/pgx/v5/pgxpool"
	"github.com/riverqueue/river"

	"github.com/nomadcode/nomadcode-core/internal/projectsync"
	"github.com/nomadcode/nomadcode-core/internal/roadmapsync"
	"github.com/nomadcode/nomadcode-core/internal/roadmapsyncpipeline"
	"github.com/nomadcode/nomadcode-core/internal/storage"
	"github.com/nomadcode/nomadcode-core/internal/workflow"
	"github.com/nomadcode/nomadcode-core/internal/workitem"
)

// fakeCreationSyncRunner records the input it was driven with so a worker test
// can assert the job args were mapped into the orchestrator input.
type fakeCreationSyncRunner struct {
	called bool
	input  roadmapsyncpipeline.SyncCreationInput
	result roadmapsyncpipeline.SyncCreationResult
	err    error
}

func (f *fakeCreationSyncRunner) SyncCreation(_ context.Context, in roadmapsyncpipeline.SyncCreationInput) (roadmapsyncpipeline.SyncCreationResult, error) {
	f.called = true
	f.input = in
	return f.result, f.err
}

func sampleCreationSyncArgs() RoadmapCreationSyncJobArgs {
	identity := roadmapsync.Identity{
		Shape:                roadmapsync.ShapeMilestone,
		RoadmapMilestonePath: "agent-roadmap/phase/p1/milestones/m1.md",
		Provider:             "plane",
		Tenant:               "general",
		Project:              "nomad",
		WorkItemID:           "NOMAD-1",
	}
	return RoadmapCreationSyncJobArgs{
		Expected:          identity,
		Ref:               workitem.Ref{Provider: "plane", ID: "NOMAD-1"},
		OriginalBody:      "<p>body</p>",
		TodoStateID:       "todo-state",
		MilestoneMarkdown: "# m",
		ProviderRevision:  "rev-1",
		RoadmapRevision:   "rev-1",
	}
}

func TestRoadmapCreationSyncWorkerCallsService(t *testing.T) {
	runner := &fakeCreationSyncRunner{
		result: roadmapsyncpipeline.SyncCreationResult{Action: roadmapsyncpipeline.SyncActionProjected},
	}
	worker := &RoadmapCreationSyncWorker{Sync: runner}

	args := sampleCreationSyncArgs()
	err := worker.Work(context.Background(), &river.Job[RoadmapCreationSyncJobArgs]{Args: args})
	if err != nil {
		t.Fatalf("Work returned error: %v", err)
	}
	if !runner.called {
		t.Fatal("expected orchestrator to be called")
	}
	if runner.input.Expected.WorkItemID != "NOMAD-1" {
		t.Errorf("unexpected work item id: %q", runner.input.Expected.WorkItemID)
	}
	if runner.input.MilestoneMarkdown != "# m" || runner.input.TodoStateID != "todo-state" {
		t.Errorf("job args not mapped into orchestrator input: %#v", runner.input)
	}
}

func TestRoadmapCreationSyncWorkerReturnsServiceError(t *testing.T) {
	wantErr := errors.New("provider failed")
	runner := &fakeCreationSyncRunner{err: wantErr}
	worker := &RoadmapCreationSyncWorker{Sync: runner}

	err := worker.Work(context.Background(), &river.Job[RoadmapCreationSyncJobArgs]{Args: sampleCreationSyncArgs()})
	if !errors.Is(err, wantErr) {
		t.Fatalf("expected service error to propagate (so River retries), got %v", err)
	}
}

type fakeTaskFinalizer struct {
	called       bool
	provider     string
	id           string
	result       []byte
	task         storage.Task
	transitioned bool
}

func (f *fakeTaskFinalizer) CompleteTaskByExternalRef(ctx context.Context, provider, id string, result json.RawMessage) (storage.Task, bool, error) {
	f.called = true
	f.provider = provider
	f.id = id
	f.result = result
	return f.task, f.transitioned, nil
}

func TestRoadmapCreationSyncWorkerFinalizesExternalTaskOnProjected(t *testing.T) {
	runner := &fakeCreationSyncRunner{
		result: roadmapsyncpipeline.SyncCreationResult{Action: roadmapsyncpipeline.SyncActionProjected, Reason: "projected successfully"},
	}
	finalizer := &fakeTaskFinalizer{
		transitioned: true,
		task: storage.Task{
			ID:       "task-auth-projected",
			Source:   "plane",
			Metadata: checkoutTaskMeta("/home/user/workspace/nomadcode/slots/000", "develop"),
		},
	}
	slotUpdater := &fakeWorkspaceSlotUpdater{}
	worker := &RoadmapCreationSyncWorker{
		Sync:          runner,
		TaskFinalizer: finalizer,
		SlotUpdater:   slotUpdater,
	}

	args := sampleCreationSyncArgs()
	err := worker.Work(context.Background(), &river.Job[RoadmapCreationSyncJobArgs]{Args: args})
	if err != nil {
		t.Fatalf("Work returned error: %v", err)
	}
	if !runner.called {
		t.Fatal("expected orchestrator to be called")
	}
	if !finalizer.called {
		t.Fatal("expected finalizer to be called on projected outcome")
	}
	if finalizer.provider != "plane" || finalizer.id != "NOMAD-1" {
		t.Errorf("unexpected finalizer arguments: provider=%q, id=%q", finalizer.provider, finalizer.id)
	}
	var resMap map[string]any
	if err := json.Unmarshal(finalizer.result, &resMap); err != nil {
		t.Fatalf("fail to unmarshal finalizer result: %v", err)
	}
	if resMap["action"] != "projected" || resMap["reason"] != "projected successfully" {
		t.Errorf("unexpected finalizer result payload: %v", resMap)
	}
	if resMap["summary"] != "projected successfully" {
		t.Errorf("expected summary payload, got %v", resMap["summary"])
	}
	if resMap[workflow.MetadataKeyAuthoringRunState] != "succeeded" {
		t.Errorf("expected authoring_run_state succeeded, got %v", resMap[workflow.MetadataKeyAuthoringRunState])
	}
	if resMap[workflow.MetadataKeyAuthoringRunUpdatedAt] == nil || resMap[workflow.MetadataKeyAuthoringRunUpdatedAt] == "" {
		t.Error("expected authoring_run_updated_at to be set")
	}
	if len(slotUpdater.calls) != 1 {
		t.Fatalf("expected 1 slot update, got %d", len(slotUpdater.calls))
	}
	if slotUpdater.calls[0].ID != 1 || slotUpdater.calls[0].State != string(projectsync.SlotStateAvailable) {
		t.Errorf("expected slot 1 -> available, got %#v", slotUpdater.calls[0])
	}
}

func TestRoadmapCreationSyncWorkerFinalizesExternalTaskOnComplete(t *testing.T) {
	runner := &fakeCreationSyncRunner{
		result: roadmapsyncpipeline.SyncCreationResult{Action: roadmapsyncpipeline.SyncActionComplete, Reason: "already complete"},
	}
	finalizer := &fakeTaskFinalizer{}
	worker := &RoadmapCreationSyncWorker{
		Sync:          runner,
		TaskFinalizer: finalizer,
	}

	args := sampleCreationSyncArgs()
	err := worker.Work(context.Background(), &river.Job[RoadmapCreationSyncJobArgs]{Args: args})
	if err != nil {
		t.Fatalf("Work returned error: %v", err)
	}
	if !runner.called {
		t.Fatal("expected orchestrator to be called")
	}
	if !finalizer.called {
		t.Fatal("expected finalizer to be called on complete outcome")
	}
	if finalizer.provider != "plane" || finalizer.id != "NOMAD-1" {
		t.Errorf("unexpected finalizer arguments: provider=%q, id=%q", finalizer.provider, finalizer.id)
	}
	var resMap map[string]any
	if err := json.Unmarshal(finalizer.result, &resMap); err != nil {
		t.Fatalf("fail to unmarshal finalizer result: %v", err)
	}
	if resMap["action"] != "complete" || resMap["reason"] != "already complete" {
		t.Errorf("unexpected finalizer result payload: %v", resMap)
	}
	if resMap["summary"] != "already complete" {
		t.Errorf("expected summary payload, got %v", resMap["summary"])
	}
	if resMap[workflow.MetadataKeyAuthoringRunState] != "succeeded" {
		t.Errorf("expected authoring_run_state succeeded, got %v", resMap[workflow.MetadataKeyAuthoringRunState])
	}
}

func TestRoadmapCreationSyncWorkerDoesNotFinalizeNotReady(t *testing.T) {
	runner := &fakeCreationSyncRunner{
		result: roadmapsyncpipeline.SyncCreationResult{Action: roadmapsyncpipeline.SyncActionNotReady, Reason: "not ready yet"},
	}
	finalizer := &fakeTaskFinalizer{}
	worker := &RoadmapCreationSyncWorker{
		Sync:          runner,
		TaskFinalizer: finalizer,
	}

	args := sampleCreationSyncArgs()
	err := worker.Work(context.Background(), &river.Job[RoadmapCreationSyncJobArgs]{Args: args})
	if err != nil {
		t.Fatalf("Work returned error: %v", err)
	}
	if !runner.called {
		t.Fatal("expected orchestrator to be called")
	}
	if finalizer.called {
		t.Fatal("expected finalizer to NOT be called on not_ready outcome")
	}
}

func TestRoadmapCreationSyncWorkerDoesNotUpdateSlotOnDuplicateComplete(t *testing.T) {
	runner := &fakeCreationSyncRunner{
		result: roadmapsyncpipeline.SyncCreationResult{Action: roadmapsyncpipeline.SyncActionComplete, Reason: "already complete"},
	}
	finalizer := &fakeTaskFinalizer{
		transitioned: false,
		task: storage.Task{
			ID:       "task-auth-complete",
			Source:   "plane",
			Metadata: checkoutTaskMeta("/home/user/workspace/nomadcode/slots/000", "develop"),
		},
	}
	slotUpdater := &fakeWorkspaceSlotUpdater{}
	worker := &RoadmapCreationSyncWorker{
		Sync:          runner,
		TaskFinalizer: finalizer,
		SlotUpdater:   slotUpdater,
	}

	args := sampleCreationSyncArgs()
	err := worker.Work(context.Background(), &river.Job[RoadmapCreationSyncJobArgs]{Args: args})
	if err != nil {
		t.Fatalf("Work returned error: %v", err)
	}
	if !finalizer.called {
		t.Fatal("expected finalizer to be called")
	}
	if len(slotUpdater.calls) != 0 {
		t.Errorf("expected 0 slot updates because task was already complete, got %d", len(slotUpdater.calls))
	}
}

func TestRoadmapCreationSyncWorkerUpdatesSlotOnCrashRecovery(t *testing.T) {
	runner := &fakeCreationSyncRunner{
		result: roadmapsyncpipeline.SyncCreationResult{Action: roadmapsyncpipeline.SyncActionComplete, Reason: "crash recovery complete"},
	}
	finalizer := &fakeTaskFinalizer{
		transitioned: true,
		task: storage.Task{
			ID:       "task-auth-recovery",
			Source:   "plane",
			Metadata: checkoutTaskMeta("/home/user/workspace/nomadcode/slots/000", "develop"),
		},
	}
	slotUpdater := &fakeWorkspaceSlotUpdater{}
	worker := &RoadmapCreationSyncWorker{
		Sync:          runner,
		TaskFinalizer: finalizer,
		SlotUpdater:   slotUpdater,
	}

	args := sampleCreationSyncArgs()
	err := worker.Work(context.Background(), &river.Job[RoadmapCreationSyncJobArgs]{Args: args})
	if err != nil {
		t.Fatalf("Work returned error: %v", err)
	}
	if !finalizer.called {
		t.Fatal("expected finalizer to be called")
	}
	if len(slotUpdater.calls) != 1 {
		t.Fatalf("expected 1 slot update for crash recovery complete, got %d", len(slotUpdater.calls))
	}
	if slotUpdater.calls[0].ID != 1 || slotUpdater.calls[0].State != string(projectsync.SlotStateAvailable) {
		t.Errorf("expected slot 1 -> available, got %#v", slotUpdater.calls[0])
	}
}

// lazyPool builds a pgxpool without dialing. pgxpool.New is lazy and only
// connects on first acquire, so it is safe for constructing the scheduler in a
// unit test that never starts the client.
func lazyPool(t *testing.T) *pgxpool.Pool {
	t.Helper()
	pool, err := pgxpool.New(context.Background(), "postgres://user:pass@127.0.0.1:1/db")
	if err != nil {
		t.Fatalf("build lazy pool: %v", err)
	}
	t.Cleanup(pool.Close)
	return pool
}

func TestSchedulerRegistersRoadmapSyncWorkerWhenConfigured(t *testing.T) {
	runner := &fakeCreationSyncRunner{}
	client, err := New(lazyPool(t), nil, nil, nil, nil, runner, nil, nil, 0, nil)
	if err != nil {
		t.Fatalf("New with creation sync runner: %v", err)
	}
	if client == nil {
		t.Fatal("expected non-nil client")
	}
	// The enqueue seam exists regardless of registration; the worker registration
	// is exercised by New succeeding with a non-nil runner (River rejects an
	// unknown job kind only at insert/work time, so construction succeeding plus
	// the worker unit test above cover the wiring without a live DB).
}

func TestSchedulerWithoutRoadmapSyncKeepsTaskWorkerOnlyBehavior(t *testing.T) {
	client, err := New(lazyPool(t), nil, nil, nil, nil, nil, nil, nil, 0, nil)
	if err != nil {
		t.Fatalf("New without creation sync runner: %v", err)
	}
	if client == nil {
		t.Fatal("expected non-nil client")
	}
}
