package ssched

import (
	"errors"
	"testing"

	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/xplan"
	"github.com/tech/sendico/payments/storage/model"
	"github.com/tech/sendico/pkg/merrors"
)

func TestSchedule_LinearFlowPicksFirstRunnable(t *testing.T) {
	runtime := New()

	out, err := runtime.Schedule(Input{
		Steps: []xplan.Step{
			step("a", nil),
			step("b", []string{"a"}),
		},
		StepExecutions: []agg.StepExecution{
			exec("a", agg.StepStatePending, 1),
			exec("b", agg.StepStatePending, 1),
		},
	})
	if err != nil {
		t.Fatalf("Schedule returned error: %v", err)
	}

	assertRunnableRefs(t, out, []string{"a"})
	assertRunnableAttempt(t, out, "a", 1)
	assertBlockedReason(t, out, "b", BlockedWaitingDependencies)
	if len(out.Skipped) != 0 {
		t.Fatalf("expected no skipped steps, got %v", out.Skipped)
	}
}

func TestSchedule_SuccessBranchSkipsFailureBranch(t *testing.T) {
	runtime := New()

	out, err := runtime.Schedule(Input{
		Steps: []xplan.Step{
			step("observe", nil),
			successStep("debit", "observe"),
			failureStep("release", "observe"),
		},
		StepExecutions: []agg.StepExecution{
			exec("observe", agg.StepStateCompleted, 1),
			exec("debit", agg.StepStatePending, 1),
			exec("release", agg.StepStatePending, 1),
		},
	})
	if err != nil {
		t.Fatalf("Schedule returned error: %v", err)
	}

	assertRunnableRefs(t, out, []string{"debit"})
	assertSkippedRefs(t, out, []string{"release"})
	release := mustExecution(t, out, "release")
	if release.State != agg.StepStateSkipped {
		t.Fatalf("release state mismatch: got=%q want=%q", release.State, agg.StepStateSkipped)
	}
	if release.CompletedAt == nil {
		t.Fatal("expected skipped step to have completed_at")
	}
}

func TestSchedule_AfterFailureWaitsWhenDependencyCanRetry(t *testing.T) {
	runtime := New()

	out, err := runtime.Schedule(Input{
		Steps: []xplan.Step{
			step("observe", nil),
			failureStep("release", "observe"),
		},
		StepExecutions: []agg.StepExecution{
			exec("observe", agg.StepStateFailed, 1),
			exec("release", agg.StepStatePending, 1),
		},
		Retry: RetryPolicy{MaxAttempts: 2},
	})
	if err != nil {
		t.Fatalf("Schedule returned error: %v", err)
	}

	assertRunnableRefs(t, out, []string{"observe"})
	assertRunnableAttempt(t, out, "observe", 2)
	assertBlockedReason(t, out, "release", BlockedWaitingDependencies)
	if len(out.Skipped) != 0 {
		t.Fatalf("expected no skipped steps, got %v", out.Skipped)
	}
}

func TestSchedule_AfterFailureRunsWhenDependencyExhausted(t *testing.T) {
	runtime := New()

	out, err := runtime.Schedule(Input{
		Steps: []xplan.Step{
			step("observe", nil),
			successStep("debit", "observe"),
			failureStep("release", "observe"),
		},
		StepExecutions: []agg.StepExecution{
			exec("observe", agg.StepStateFailed, 2),
			exec("debit", agg.StepStatePending, 1),
			exec("release", agg.StepStatePending, 1),
		},
		Retry: RetryPolicy{MaxAttempts: 2},
	})
	if err != nil {
		t.Fatalf("Schedule returned error: %v", err)
	}

	observe := mustExecution(t, out, "observe")
	if observe.State != agg.StepStateNeedsAttention {
		t.Fatalf("observe state mismatch: got=%q want=%q", observe.State, agg.StepStateNeedsAttention)
	}
	assertRunnableRefs(t, out, []string{"release"})
	assertSkippedRefs(t, out, []string{"debit"})
	assertBlockedReason(t, out, "observe", BlockedNeedsAttention)
}

func TestSchedule_SendFailureRunsSendFailureReleaseAndSkipsObserveBranches(t *testing.T) {
	runtime := New()

	out, err := runtime.Schedule(Input{
		Steps: []xplan.Step{
			step("send", nil),
			step("observe", []string{"send"}),
			successStep("debit", "observe"),
			failureStep("release_send", "send"),
			failureStep("release_observe", "observe"),
		},
		StepExecutions: []agg.StepExecution{
			exec("send", agg.StepStateFailed, 2),
			exec("observe", agg.StepStatePending, 1),
			exec("debit", agg.StepStatePending, 1),
			exec("release_send", agg.StepStatePending, 1),
			exec("release_observe", agg.StepStatePending, 1),
		},
		Retry: RetryPolicy{MaxAttempts: 2},
	})
	if err != nil {
		t.Fatalf("Schedule returned error: %v", err)
	}

	send := mustExecution(t, out, "send")
	if send.State != agg.StepStateNeedsAttention {
		t.Fatalf("send state mismatch: got=%q want=%q", send.State, agg.StepStateNeedsAttention)
	}

	assertRunnableRefs(t, out, []string{"release_send"})
	assertSkippedRefs(t, out, []string{"observe", "debit", "release_observe"})
	assertBlockedReason(t, out, "send", BlockedNeedsAttention)

	releaseSend := mustExecution(t, out, "release_send")
	if releaseSend.State != agg.StepStatePending {
		t.Fatalf("release_send state mismatch: got=%q want=%q", releaseSend.State, agg.StepStatePending)
	}
}

func TestSchedule_ObserveFailureRunsObserveFailureReleaseAndSkipsSendFailureRelease(t *testing.T) {
	runtime := New()

	out, err := runtime.Schedule(Input{
		Steps: []xplan.Step{
			step("send", nil),
			step("observe", []string{"send"}),
			successStep("debit", "observe"),
			failureStep("release_send", "send"),
			failureStep("release_observe", "observe"),
		},
		StepExecutions: []agg.StepExecution{
			exec("send", agg.StepStateCompleted, 1),
			exec("observe", agg.StepStateFailed, 2),
			exec("debit", agg.StepStatePending, 1),
			exec("release_send", agg.StepStatePending, 1),
			exec("release_observe", agg.StepStatePending, 1),
		},
		Retry: RetryPolicy{MaxAttempts: 2},
	})
	if err != nil {
		t.Fatalf("Schedule returned error: %v", err)
	}

	observe := mustExecution(t, out, "observe")
	if observe.State != agg.StepStateNeedsAttention {
		t.Fatalf("observe state mismatch: got=%q want=%q", observe.State, agg.StepStateNeedsAttention)
	}

	assertRunnableRefs(t, out, []string{"release_observe"})
	assertSkippedRefs(t, out, []string{"debit", "release_send"})
	assertBlockedReason(t, out, "observe", BlockedNeedsAttention)

	releaseObserve := mustExecution(t, out, "release_observe")
	if releaseObserve.State != agg.StepStatePending {
		t.Fatalf("release_observe state mismatch: got=%q want=%q", releaseObserve.State, agg.StepStatePending)
	}
}

func TestSchedule_RetryExhaustedPromotesNeedsAttention(t *testing.T) {
	runtime := New()

	out, err := runtime.Schedule(Input{
		Steps: []xplan.Step{
			step("single", nil),
		},
		StepExecutions: []agg.StepExecution{
			exec("single", agg.StepStateFailed, 1),
		},
		Retry: RetryPolicy{MaxAttempts: 1},
	})
	if err != nil {
		t.Fatalf("Schedule returned error: %v", err)
	}

	single := mustExecution(t, out, "single")
	if single.State != agg.StepStateNeedsAttention {
		t.Fatalf("single state mismatch: got=%q want=%q", single.State, agg.StepStateNeedsAttention)
	}
	assertBlockedReason(t, out, "single", BlockedNeedsAttention)
	if len(out.Runnable) != 0 {
		t.Fatalf("expected no runnable steps, got %d", len(out.Runnable))
	}
}

func TestSchedule_FailedDependencySkipsImmediateDependents(t *testing.T) {
	runtime := New()

	out, err := runtime.Schedule(Input{
		Steps: []xplan.Step{
			step("a", nil),
			step("b", []string{"a"}),
		},
		StepExecutions: []agg.StepExecution{
			exec("a", agg.StepStateFailed, 1),
			exec("b", agg.StepStatePending, 1),
		},
		Retry: RetryPolicy{MaxAttempts: 1},
	})
	if err != nil {
		t.Fatalf("Schedule returned error: %v", err)
	}

	a := mustExecution(t, out, "a")
	if a.State != agg.StepStateNeedsAttention {
		t.Fatalf("a state mismatch: got=%q want=%q", a.State, agg.StepStateNeedsAttention)
	}
	assertSkippedRefs(t, out, []string{"b"})
	assertBlockedReason(t, out, "a", BlockedNeedsAttention)
}

func TestSchedule_SkippedDependencyAlsoSkipsDependent(t *testing.T) {
	runtime := New()

	out, err := runtime.Schedule(Input{
		Steps: []xplan.Step{
			step("guard", nil),
			step("send", []string{"guard"}),
			step("observe", []string{"send"}),
		},
		StepExecutions: []agg.StepExecution{
			exec("guard", agg.StepStateNeedsAttention, 2),
			exec("send", agg.StepStatePending, 1),
			exec("observe", agg.StepStatePending, 1),
		},
	})
	if err != nil {
		t.Fatalf("Schedule returned error: %v", err)
	}

	if len(out.Runnable) != 0 {
		t.Fatalf("expected no runnable steps, got %d", len(out.Runnable))
	}
	assertSkippedRefs(t, out, []string{"send", "observe"})
	assertBlockedReason(t, out, "guard", BlockedNeedsAttention)

	send := mustExecution(t, out, "send")
	if send.State != agg.StepStateSkipped {
		t.Fatalf("send state mismatch: got=%q want=%q", send.State, agg.StepStateSkipped)
	}
	observe := mustExecution(t, out, "observe")
	if observe.State != agg.StepStateSkipped {
		t.Fatalf("observe state mismatch: got=%q want=%q", observe.State, agg.StepStateSkipped)
	}
}

func TestSchedule_ValidationErrors(t *testing.T) {
	runtime := New()

	tests := []struct {
		name string
		in   Input
	}{
		{
			name: "missing steps",
			in: Input{
				StepExecutions: []agg.StepExecution{exec("a", agg.StepStatePending, 1)},
			},
		},
		{
			name: "step dependency unknown",
			in: Input{
				Steps: []xplan.Step{
					step("a", []string{"missing"}),
				},
			},
		},
		{
			name: "unknown execution state",
			in: Input{
				Steps: []xplan.Step{
					step("a", nil),
				},
				StepExecutions: []agg.StepExecution{
					{StepRef: "a", StepCode: "a", State: agg.StepState("bad_state"), Attempt: 1},
				},
			},
		},
		{
			name: "execution ref unknown in graph",
			in: Input{
				Steps: []xplan.Step{
					step("a", nil),
				},
				StepExecutions: []agg.StepExecution{
					exec("x", agg.StepStatePending, 1),
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			_, err := runtime.Schedule(tt.in)
			if !errors.Is(err, merrors.ErrInvalidArg) {
				t.Fatalf("expected invalid argument error, got %v", err)
			}
		})
	}
}

func step(ref string, deps []string) xplan.Step {
	return xplan.Step{
		StepRef:   ref,
		StepCode:  ref,
		DependsOn: deps,
	}
}

func successStep(ref, dep string) xplan.Step {
	return xplan.Step{
		StepRef:      ref,
		StepCode:     ref,
		DependsOn:    []string{dep},
		CommitPolicy: model.CommitPolicyAfterSuccess,
		CommitAfter:  []string{dep},
	}
}

func failureStep(ref, dep string) xplan.Step {
	return xplan.Step{
		StepRef:      ref,
		StepCode:     ref,
		DependsOn:    []string{dep},
		CommitPolicy: model.CommitPolicyAfterFailure,
		CommitAfter:  []string{dep},
	}
}

func exec(ref string, state agg.StepState, attempt uint32) agg.StepExecution {
	return agg.StepExecution{
		StepRef:  ref,
		StepCode: ref,
		State:    state,
		Attempt:  attempt,
	}
}

func mustExecution(t *testing.T, out *Output, stepRef string) agg.StepExecution {
	t.Helper()
	for i := range out.StepExecutions {
		if out.StepExecutions[i].StepRef == stepRef {
			return out.StepExecutions[i]
		}
	}
	t.Fatalf("missing execution for step_ref %q", stepRef)
	return agg.StepExecution{}
}

func assertRunnableRefs(t *testing.T, out *Output, want []string) {
	t.Helper()
	if len(out.Runnable) != len(want) {
		t.Fatalf("runnable count mismatch: got=%d want=%d", len(out.Runnable), len(want))
	}
	for i := range want {
		if out.Runnable[i].StepRef != want[i] {
			t.Fatalf("runnable[%d] mismatch: got=%q want=%q", i, out.Runnable[i].StepRef, want[i])
		}
	}
}

func assertRunnableAttempt(t *testing.T, out *Output, stepRef string, want uint32) {
	t.Helper()
	for i := range out.Runnable {
		if out.Runnable[i].StepRef == stepRef {
			if out.Runnable[i].Attempt != want {
				t.Fatalf("runnable attempt mismatch for %q: got=%d want=%d", stepRef, out.Runnable[i].Attempt, want)
			}
			return
		}
	}
	t.Fatalf("runnable step %q not found", stepRef)
}

func assertBlockedReason(t *testing.T, out *Output, stepRef string, want BlockedReason) {
	t.Helper()
	for i := range out.Blocked {
		if out.Blocked[i].StepRef != stepRef {
			continue
		}
		if out.Blocked[i].Reason != want {
			t.Fatalf("blocked reason mismatch for %q: got=%q want=%q", stepRef, out.Blocked[i].Reason, want)
		}
		return
	}
	t.Fatalf("blocked step %q not found", stepRef)
}

func assertSkippedRefs(t *testing.T, out *Output, want []string) {
	t.Helper()
	if len(out.Skipped) != len(want) {
		t.Fatalf("skipped count mismatch: got=%d want=%d", len(out.Skipped), len(want))
	}
	for i := range want {
		if out.Skipped[i] != want[i] {
			t.Fatalf("skipped[%d] mismatch: got=%q want=%q", i, out.Skipped[i], want[i])
		}
	}
}
