package erecon

import (
	"errors"
	"testing"
	"time"

	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
	"github.com/tech/sendico/pkg/merrors"
	paymenttypes "github.com/tech/sendico/pkg/payments/types"
	"go.uber.org/zap"
)

func TestReconcile_GatewayWaiting_UpdatesRunningAndRefs(t *testing.T) {
	now := time.Date(2026, time.January, 10, 11, 12, 13, 0, time.UTC)
	reconciler := New(Dependencies{Logger: zap.NewNop(), Now: func() time.Time { return now }})

	in := &agg.Payment{
		PaymentRef: "p1",
		State:      agg.StateCreated,
		Version:    7,
		StepExecutions: []agg.StepExecution{
			{StepRef: "s1", StepCode: "send", State: agg.StepStatePending, Attempt: 1},
		},
	}

	out, err := reconciler.Reconcile(Input{
		Payment: in,
		Event: Event{
			Gateway: &GatewayEvent{
				StepRef:           "s1",
				OperationRef:      "op-1",
				TransferRef:       "tx-1",
				GatewayInstanceID: "gw-1",
				Status:            GatewayStatusWaiting,
				ExecutedMoney:     &paymenttypes.Money{Amount: "5.84", Currency: "USDT"},
			},
		},
	})
	if err != nil {
		t.Fatalf("Reconcile returned error: %v", err)
	}
	if !out.StepChanged {
		t.Fatal("expected step_changed")
	}
	if !out.AggregateChanged {
		t.Fatal("expected aggregate_changed")
	}

	got := out.Payment.StepExecutions[0]
	if got.State != agg.StepStateRunning {
		t.Fatalf("step state mismatch: got=%q want=%q", got.State, agg.StepStateRunning)
	}
	if got.StartedAt == nil || !got.StartedAt.Equal(now) {
		t.Fatalf("started_at mismatch: got=%v want=%v", got.StartedAt, now)
	}
	if got.CompletedAt != nil {
		t.Fatalf("expected nil completed_at, got %v", got.CompletedAt)
	}
	if !hasRef(got.ExternalRefs, agg.ExternalRef{GatewayInstanceID: "gw-1", Kind: ExternalRefKindOperation, Ref: "op-1"}) {
		t.Fatalf("expected operation_ref external reference")
	}
	if !hasRef(got.ExternalRefs, agg.ExternalRef{GatewayInstanceID: "gw-1", Kind: ExternalRefKindTransfer, Ref: "tx-1"}) {
		t.Fatalf("expected transfer_ref external reference")
	}
	if got.ExecutedMoney == nil {
		t.Fatal("expected executed money to be mapped")
	}
	if gotAmt, want := got.ExecutedMoney.Amount, "5.84"; gotAmt != want {
		t.Fatalf("executed money amount mismatch: got=%q want=%q", gotAmt, want)
	}
	if gotCur, want := got.ExecutedMoney.Currency, "USDT"; gotCur != want {
		t.Fatalf("executed money currency mismatch: got=%q want=%q", gotCur, want)
	}
	if out.Payment.State != agg.StateExecuting {
		t.Fatalf("aggregate state mismatch: got=%q want=%q", out.Payment.State, agg.StateExecuting)
	}
	if out.Payment.Version != 8 {
		t.Fatalf("version mismatch: got=%d want=%d", out.Payment.Version, 8)
	}

	if in.StepExecutions[0].State != agg.StepStatePending {
		t.Fatalf("input payment was mutated")
	}
}

func TestReconcile_GatewaySuccess_SettlesPayment(t *testing.T) {
	now := time.Date(2026, time.January, 10, 11, 12, 13, 0, time.UTC)
	reconciler := New(Dependencies{Logger: zap.NewNop(), Now: func() time.Time { return now }})

	out, err := reconciler.Reconcile(Input{
		Payment: &agg.Payment{
			PaymentRef: "p1",
			State:      agg.StateCreated,
			Version:    1,
			StepExecutions: []agg.StepExecution{
				{StepRef: "s1", StepCode: "observe", State: agg.StepStatePending, Attempt: 1},
			},
		},
		Event: Event{
			Gateway: &GatewayEvent{
				StepRef:      "s1",
				Status:       GatewayStatusSuccess,
				OperationRef: "op-1",
			},
		},
	})
	if err != nil {
		t.Fatalf("Reconcile returned error: %v", err)
	}
	step := out.Payment.StepExecutions[0]
	if step.State != agg.StepStateCompleted {
		t.Fatalf("step state mismatch: got=%q want=%q", step.State, agg.StepStateCompleted)
	}
	if step.CompletedAt == nil || !step.CompletedAt.Equal(now) {
		t.Fatalf("completed_at mismatch: got=%v want=%v", step.CompletedAt, now)
	}
	if out.Payment.State != agg.StateSettled {
		t.Fatalf("aggregate state mismatch: got=%q want=%q", out.Payment.State, agg.StateSettled)
	}
}

func TestReconcile_GatewayExecutedMoney_DoesNotOverrideExisting(t *testing.T) {
	now := time.Date(2026, time.January, 10, 11, 12, 13, 0, time.UTC)
	reconciler := New(Dependencies{Logger: zap.NewNop(), Now: func() time.Time { return now }})

	out, err := reconciler.Reconcile(Input{
		Payment: &agg.Payment{
			PaymentRef: "p1",
			State:      agg.StateCreated,
			Version:    1,
			StepExecutions: []agg.StepExecution{
				{
					StepRef:  "s1",
					StepCode: "observe",
					State:    agg.StepStatePending,
					Attempt:  1,
					ExecutedMoney: &paymenttypes.Money{
						Amount:   "76.50",
						Currency: "RUB",
					},
				},
			},
		},
		Event: Event{
			Gateway: &GatewayEvent{
				StepRef: "s1",
				Status:  GatewayStatusSuccess,
				ExecutedMoney: &paymenttypes.Money{
					Amount:   "7650",
					Currency: "RUB",
				},
			},
		},
	})
	if err != nil {
		t.Fatalf("Reconcile returned error: %v", err)
	}
	step := out.Payment.StepExecutions[0]
	if step.ExecutedMoney == nil {
		t.Fatal("expected executed money")
	}
	if got, want := step.ExecutedMoney.Amount, "76.50"; got != want {
		t.Fatalf("executed money amount mismatch: got=%q want=%q", got, want)
	}
	if got, want := step.ExecutedMoney.Currency, "RUB"; got != want {
		t.Fatalf("executed money currency mismatch: got=%q want=%q", got, want)
	}
}

func TestReconcile_GatewayFailureMapping(t *testing.T) {
	now := time.Date(2026, time.January, 10, 11, 12, 13, 0, time.UTC)
	reconciler := New(Dependencies{Logger: zap.NewNop(), Now: func() time.Time { return now }})

	retryable := true
	out, err := reconciler.Reconcile(Input{
		Payment: &agg.Payment{
			PaymentRef: "p1",
			State:      agg.StateExecuting,
			StepExecutions: []agg.StepExecution{
				{StepRef: "s1", StepCode: "observe", State: agg.StepStateRunning, Attempt: 1},
			},
		},
		Event: Event{
			Gateway: &GatewayEvent{
				StepRef:     "s1",
				Status:      GatewayStatusFailed,
				Retryable:   &retryable,
				FailureCode: "gw_timeout",
				FailureMsg:  "timeout",
			},
		},
	})
	if err != nil {
		t.Fatalf("Reconcile returned error: %v", err)
	}
	step := out.Payment.StepExecutions[0]
	if step.State != agg.StepStateFailed {
		t.Fatalf("step state mismatch: got=%q want=%q", step.State, agg.StepStateFailed)
	}
	if step.FailureCode != "gw_timeout" || step.FailureMsg != "timeout" {
		t.Fatalf("failure details mismatch: code=%q msg=%q", step.FailureCode, step.FailureMsg)
	}
	if out.Payment.State != agg.StateExecuting {
		t.Fatalf("aggregate state mismatch: got=%q want=%q", out.Payment.State, agg.StateExecuting)
	}

	nonRetryable := false
	out, err = reconciler.Reconcile(Input{
		Payment: &agg.Payment{
			PaymentRef: "p2",
			State:      agg.StateExecuting,
			StepExecutions: []agg.StepExecution{
				{StepRef: "s1", StepCode: "observe", State: agg.StepStateRunning, Attempt: 1},
			},
		},
		Event: Event{
			Gateway: &GatewayEvent{
				StepRef:     "s1",
				Status:      GatewayStatusFailed,
				Retryable:   &nonRetryable,
				FailureCode: "gw_rejected",
				FailureMsg:  "rejected",
			},
		},
	})
	if err != nil {
		t.Fatalf("Reconcile returned error: %v", err)
	}
	step = out.Payment.StepExecutions[0]
	if step.State != agg.StepStateNeedsAttention {
		t.Fatalf("step state mismatch: got=%q want=%q", step.State, agg.StepStateNeedsAttention)
	}
	if out.Payment.State != agg.StateNeedsAttention {
		t.Fatalf("aggregate state mismatch: got=%q want=%q", out.Payment.State, agg.StateNeedsAttention)
	}
}

func TestReconcile_LedgerTerminalFailure_ForcesAggregateFailed(t *testing.T) {
	now := time.Date(2026, time.January, 10, 11, 12, 13, 0, time.UTC)
	reconciler := New(Dependencies{Logger: zap.NewNop(), Now: func() time.Time { return now }})

	out, err := reconciler.Reconcile(Input{
		Payment: &agg.Payment{
			PaymentRef: "p1",
			State:      agg.StateExecuting,
			StepExecutions: []agg.StepExecution{
				{
					StepRef: "s1", StepCode: "ledger.debit", State: agg.StepStateRunning, Attempt: 1,
					ExternalRefs: []agg.ExternalRef{{Kind: ExternalRefKindLedger, Ref: "entry-1"}},
				},
			},
		},
		Event: Event{
			Ledger: &LedgerEvent{
				EntryRef:        "entry-1",
				Status:          LedgerStatusFailed,
				FailureCode:     "ledger_declined",
				TerminalFailure: true,
			},
		},
	})
	if err != nil {
		t.Fatalf("Reconcile returned error: %v", err)
	}
	if out.Payment.StepExecutions[0].State != agg.StepStateFailed {
		t.Fatalf("step state mismatch: got=%q want=%q", out.Payment.StepExecutions[0].State, agg.StepStateFailed)
	}
	if out.Payment.State != agg.StateFailed {
		t.Fatalf("aggregate state mismatch: got=%q want=%q", out.Payment.State, agg.StateFailed)
	}
}

func TestReconcile_CardMatchByExternalRef(t *testing.T) {
	reconciler := New(Dependencies{Logger: zap.NewNop(), Now: defaultNow})

	out, err := reconciler.Reconcile(Input{
		Payment: &agg.Payment{
			PaymentRef: "p1",
			State:      agg.StateExecuting,
			StepExecutions: []agg.StepExecution{
				{
					StepRef: "s1", StepCode: "card.observe", State: agg.StepStateRunning, Attempt: 1,
					ExternalRefs: []agg.ExternalRef{
						{Kind: ExternalRefKindCardPayout, Ref: "payout-1"},
					},
				},
			},
		},
		Event: Event{
			Card: &CardEvent{
				PayoutRef: "payout-1",
				Status:    CardStatusSuccess,
			},
		},
	})
	if err != nil {
		t.Fatalf("Reconcile returned error: %v", err)
	}
	if out.MatchedStepRef != "s1" {
		t.Fatalf("matched step mismatch: got=%q want=%q", out.MatchedStepRef, "s1")
	}
	if out.Payment.StepExecutions[0].State != agg.StepStateCompleted {
		t.Fatalf("step state mismatch: got=%q want=%q", out.Payment.StepExecutions[0].State, agg.StepStateCompleted)
	}
}

func TestReconcile_MatchingErrors(t *testing.T) {
	reconciler := New(Dependencies{Logger: zap.NewNop(), Now: defaultNow})

	_, err := reconciler.Reconcile(Input{
		Payment: &agg.Payment{
			PaymentRef: "p1",
			State:      agg.StateExecuting,
			StepExecutions: []agg.StepExecution{
				{
					StepRef: "s1", StepCode: "a", State: agg.StepStateRunning,
					ExternalRefs: []agg.ExternalRef{{Kind: ExternalRefKindTransfer, Ref: "tx-1"}},
				},
				{
					StepRef: "s2", StepCode: "b", State: agg.StepStateRunning,
					ExternalRefs: []agg.ExternalRef{{Kind: ExternalRefKindTransfer, Ref: "tx-1"}},
				},
			},
		},
		Event: Event{
			Gateway: &GatewayEvent{
				TransferRef: "tx-1",
				Status:      GatewayStatusSuccess,
			},
		},
	})
	if !errors.Is(err, ErrAmbiguousStepMatch) {
		t.Fatalf("expected ErrAmbiguousStepMatch, got %v", err)
	}

	_, err = reconciler.Reconcile(Input{
		Payment: &agg.Payment{
			PaymentRef:     "p1",
			State:          agg.StateExecuting,
			StepExecutions: []agg.StepExecution{{StepRef: "s1", StepCode: "a", State: agg.StepStateRunning}},
		},
		Event: Event{
			Gateway: &GatewayEvent{
				TransferRef: "missing",
				Status:      GatewayStatusSuccess,
			},
		},
	})
	if !errors.Is(err, ErrStepNotFound) {
		t.Fatalf("expected ErrStepNotFound, got %v", err)
	}
}

func TestReconcile_ValidationErrors(t *testing.T) {
	reconciler := New(Dependencies{Logger: zap.NewNop(), Now: defaultNow})

	tests := []struct {
		name string
		in   Input
	}{
		{
			name: "missing payment",
			in: Input{
				Event: Event{Gateway: &GatewayEvent{StepRef: "s1", Status: GatewayStatusSuccess}},
			},
		},
		{
			name: "missing step executions",
			in: Input{
				Payment: &agg.Payment{},
				Event:   Event{Gateway: &GatewayEvent{StepRef: "s1", Status: GatewayStatusSuccess}},
			},
		},
		{
			name: "multiple payloads",
			in: Input{
				Payment: &agg.Payment{
					StepExecutions: []agg.StepExecution{{StepRef: "s1", StepCode: "a", State: agg.StepStatePending}},
				},
				Event: Event{
					Gateway: &GatewayEvent{StepRef: "s1", Status: GatewayStatusSuccess},
					Ledger:  &LedgerEvent{StepRef: "s1", Status: LedgerStatusPosted},
				},
			},
		},
		{
			name: "invalid status",
			in: Input{
				Payment: &agg.Payment{
					StepExecutions: []agg.StepExecution{{StepRef: "s1", StepCode: "a", State: agg.StepStatePending}},
				},
				Event: Event{
					Card: &CardEvent{StepRef: "s1", Status: CardStatus("bad")},
				},
			},
		},
	}

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

func hasRef(refs []agg.ExternalRef, wanted agg.ExternalRef) bool {
	for i := range refs {
		ref := refs[i]
		if ref.Kind != wanted.Kind {
			continue
		}
		if ref.Ref != wanted.Ref {
			continue
		}
		if ref.GatewayInstanceID != wanted.GatewayInstanceID {
			continue
		}
		return true
	}
	return false
}
