package controlplane

import (
	"errors"
	"testing"
)

// TestTrackStepRecordsErrorWithoutPropagating confirms the historical
// best-effort contract: trackStep stores the error on the job but never
// surfaces it to the caller. This pins the existing behavior so a future
// edit to trackStep cannot quietly break callers that intentionally rely
// on the best-effort semantics.
func TestTrackStepRecordsErrorWithoutPropagating(t *testing.T) {
	// Arrange
	j := &JobResult{Operation: "test"}
	want := errors.New("boom")

	// Act
	j.trackStep("BestEffortStep", func() error { return want })

	// Assert
	if len(j.Steps) != 1 {
		t.Fatalf("trackStep recorded %d steps, want 1", len(j.Steps))
	}
	if !errors.Is(j.Steps[0].Error, want) {
		t.Fatalf("trackStep stored error %v, want %v", j.Steps[0].Error, want)
	}
	if j.Status != "" {
		t.Fatalf("trackStep flipped Status to %q, want empty (best-effort must not flag the job as failed)", j.Status)
	}
	if j.Error != nil {
		t.Fatalf("trackStep set j.Error=%v, want nil (best-effort must not surface the error on the job)", j.Error)
	}
}

// TestTrackStepFatalReturnsErrorAndFlagsJob is the regression test for
// IANTHEREAL P1 (PR #271 Round 8): cutover migrations whose failure must
// gate ApplyAll need a way to abort RunRepair. trackStepFatal MUST return
// the wrapped error and flag the job as failed.
func TestTrackStepFatalReturnsErrorAndFlagsJob(t *testing.T) {
	// Arrange
	j := &JobResult{Operation: "test"}
	want := errors.New("migration aborted")

	// Act
	err := j.trackStepFatal("CutoverStep", func() error { return want })

	// Assert
	if err == nil {
		t.Fatal("trackStepFatal returned nil for failing step, want wrapped error (regression: RunRepair would silently complete and ApplyAll would proceed against unmigrated data)")
	}
	if !errors.Is(err, want) {
		t.Fatalf("trackStepFatal returned %v, want wrapping %v", err, want)
	}
	if len(j.Steps) != 1 || j.Steps[0].Name != "CutoverStep" {
		t.Fatalf("trackStepFatal failed to record step result: %+v", j.Steps)
	}
	if !errors.Is(j.Steps[0].Error, want) {
		t.Fatalf("trackStepFatal stored step error %v, want %v", j.Steps[0].Error, want)
	}
	if j.Status != "failed" {
		t.Fatalf("trackStepFatal Status=%q, want \"failed\"", j.Status)
	}
	if !errors.Is(j.Error, want) {
		t.Fatalf("trackStepFatal j.Error=%v, want %v", j.Error, want)
	}
}

// TestTrackStepFatalSuccessIsTransparent confirms the happy path stays
// transparent: a passing fatal step returns nil, records timing, and does
// NOT flip Status to "failed" (the caller is responsible for setting
// Status="completed" once all steps finish).
func TestTrackStepFatalSuccessIsTransparent(t *testing.T) {
	// Arrange
	j := &JobResult{Operation: "test"}

	// Act
	err := j.trackStepFatal("PassingStep", func() error { return nil })

	// Assert
	if err != nil {
		t.Fatalf("trackStepFatal returned %v on success, want nil", err)
	}
	if len(j.Steps) != 1 || j.Steps[0].Error != nil {
		t.Fatalf("trackStepFatal recorded unexpected step state: %+v", j.Steps)
	}
	if j.Status == "failed" {
		t.Fatalf("trackStepFatal flipped Status to \"failed\" on a passing step")
	}
	if j.Error != nil {
		t.Fatalf("trackStepFatal set j.Error=%v on success, want nil", j.Error)
	}
}

// TestTrackStepFatalAbortsBeforeLaterSteps confirms the operational
// invariant the cutover migrations rely on: when a fatal step fails inside
// a sequence, the caller's `if err != nil { return err }` shortcut prevents
// any later steps (fatal or best-effort) from running. This is the property
// that protects ApplyAll: NormalizeProcessActionLiteralFieldValue failure
// must abort RunRepair so BackfillWorkflowAlertTemplateIds and the rest
// never run on a half-migrated database, and ApplyAll never reaches
// ApplyPacks / Verify.
func TestTrackStepFatalAbortsBeforeLaterSteps(t *testing.T) {
	// Arrange
	j := &JobResult{Operation: "test"}
	bestEffortRan := false
	laterFatalRan := false
	want := errors.New("first cutover failed")

	// Act — mimic RunRepair's call shape
	runErr := func() error {
		if err := j.trackStepFatal("FirstCutover", func() error { return want }); err != nil {
			return err
		}
		// These must NOT run after a fatal failure.
		j.trackStep("LaterBestEffort", func() error {
			bestEffortRan = true
			return nil
		})
		if err := j.trackStepFatal("LaterCutover", func() error {
			laterFatalRan = true
			return nil
		}); err != nil {
			return err
		}
		return nil
	}()

	// Assert
	if !errors.Is(runErr, want) {
		t.Fatalf("sequence returned %v, want wrapping %v", runErr, want)
	}
	if bestEffortRan {
		t.Fatal("LaterBestEffort ran after a fatal step failure — regression: failed cutover would leave downstream best-effort work executing on broken state")
	}
	if laterFatalRan {
		t.Fatal("LaterCutover ran after a fatal step failure — regression: ApplyAll would proceed past a corrupt cutover into ApplyPacks / Verify")
	}
	if len(j.Steps) != 1 {
		t.Fatalf("recorded %d steps, want exactly 1 (later steps must not be tracked once we've aborted)", len(j.Steps))
	}
}
