package sexec

import (
	"context"
	"errors"
	"github.com/tech/sendico/pkg/discovery"
	"strings"
	"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/pkg/merrors"
)

func TestExecute_DispatchLedger(t *testing.T) {
	ledger := &fakeLedgerExecutor{}
	registry := New(Dependencies{Ledger: ledger})

	out, err := registry.Execute(context.Background(), ExecuteInput{
		Payment:       &agg.Payment{PaymentRef: "p1"},
		Step:          xplan.Step{StepRef: "s1", StepCode: "ledger.debit", Action: discovery.RailOperationDebit, Rail: discovery.RailLedger},
		StepExecution: agg.StepExecution{},
	})
	if err != nil {
		t.Fatalf("Execute returned error: %v", err)
	}
	if out == nil {
		t.Fatal("expected output")
	}
	if ledger.calls != 1 {
		t.Fatalf("expected ledger executor to be called once, got %d", ledger.calls)
	}
	if got, want := ledger.lastReq.StepExecution.Attempt, uint32(1); got != want {
		t.Fatalf("attempt mismatch: got=%d want=%d", got, want)
	}
	if got, want := ledger.lastReq.StepExecution.StepRef, "s1"; got != want {
		t.Fatalf("step_ref mismatch: got=%q want=%q", got, want)
	}
}

func TestExecute_DispatchRailsAndObserve(t *testing.T) {
	crypto := &fakeCryptoExecutor{}
	provider := &fakeProviderSettlementExecutor{}
	card := &fakeCardPayoutExecutor{}
	observe := &fakeObserveConfirmExecutor{}
	registry := New(Dependencies{
		Crypto:             crypto,
		ProviderSettlement: provider,
		CardPayout:         card,
		ObserveConfirm:     observe,
	})

	tests := []struct {
		name      string
		step      xplan.Step
		wantCalls func(t *testing.T)
	}{
		{
			name: "send crypto",
			step: xplan.Step{
				StepRef: "s1", StepCode: "crypto.send", Action: discovery.RailOperationSend, Rail: discovery.RailCrypto,
			},
			wantCalls: func(t *testing.T) {
				t.Helper()
				if crypto.calls != 1 || provider.calls != 0 || card.calls != 0 || observe.calls != 0 {
					t.Fatalf("unexpected call counters crypto=%d provider=%d card=%d observe=%d", crypto.calls, provider.calls, card.calls, observe.calls)
				}
			},
		},
		{
			name: "fx convert provider settlement",
			step: xplan.Step{
				StepRef: "s2", StepCode: "provider.fx_convert", Action: discovery.RailOperationFXConvert, Rail: discovery.RailProviderSettlement,
			},
			wantCalls: func(t *testing.T) {
				t.Helper()
				if crypto.calls != 1 || provider.calls != 1 || card.calls != 0 || observe.calls != 0 {
					t.Fatalf("unexpected call counters crypto=%d provider=%d card=%d observe=%d", crypto.calls, provider.calls, card.calls, observe.calls)
				}
			},
		},
		{
			name: "send card payout",
			step: xplan.Step{
				StepRef: "s3", StepCode: "card.send", Action: discovery.RailOperationSend, Rail: discovery.RailCardPayout,
			},
			wantCalls: func(t *testing.T) {
				t.Helper()
				if crypto.calls != 1 || provider.calls != 1 || card.calls != 1 || observe.calls != 0 {
					t.Fatalf("unexpected call counters crypto=%d provider=%d card=%d observe=%d", crypto.calls, provider.calls, card.calls, observe.calls)
				}
			},
		},
		{
			name: "observe confirm",
			step: xplan.Step{
				StepRef: "s4", StepCode: "observe", Action: discovery.RailOperationObserveConfirm, Rail: discovery.RailCardPayout,
			},
			wantCalls: func(t *testing.T) {
				t.Helper()
				if crypto.calls != 1 || provider.calls != 1 || card.calls != 1 || observe.calls != 1 {
					t.Fatalf("unexpected call counters crypto=%d provider=%d card=%d observe=%d", crypto.calls, provider.calls, card.calls, observe.calls)
				}
			},
		},
		{
			name: "crypto fee",
			step: xplan.Step{
				StepRef: "s5", StepCode: "crypto.fee", Action: discovery.RailOperationFee, Rail: discovery.RailCrypto,
			},
			wantCalls: func(t *testing.T) {
				t.Helper()
				if crypto.calls != 2 || provider.calls != 1 || card.calls != 1 || observe.calls != 1 {
					t.Fatalf("unexpected call counters crypto=%d provider=%d card=%d observe=%d", crypto.calls, provider.calls, card.calls, observe.calls)
				}
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			_, err := registry.Execute(context.Background(), ExecuteInput{
				Payment:       &agg.Payment{PaymentRef: "p1"},
				Step:          tt.step,
				StepExecution: agg.StepExecution{StepRef: tt.step.StepRef, StepCode: tt.step.StepCode, Attempt: 1},
			})
			if err != nil {
				t.Fatalf("Execute returned error: %v", err)
			}
			tt.wantCalls(t)
		})
	}
}

func TestExecute_DispatchGuard(t *testing.T) {
	guard := &fakeGuardExecutor{}
	registry := New(Dependencies{Guard: guard})

	out, err := registry.Execute(context.Background(), ExecuteInput{
		Payment: &agg.Payment{PaymentRef: "p1"},
		Step: xplan.Step{
			StepRef:   xplan.QuoteReadinessGuardStepRef,
			StepCode:  string(xplan.GuardOperationQuoteReadinessGuard),
			Kind:      xplan.StepKindLiquidityCheck,
			Action:    discovery.RailOperationUnspecified,
			Rail:      discovery.RailUnspecified,
			DependsOn: nil,
		},
		StepExecution: agg.StepExecution{
			StepRef:  xplan.QuoteReadinessGuardStepRef,
			StepCode: string(xplan.GuardOperationQuoteReadinessGuard),
			Attempt:  1,
		},
	})
	if err != nil {
		t.Fatalf("Execute returned error: %v", err)
	}
	if out == nil {
		t.Fatal("expected output")
	}
	if guard.calls != 1 {
		t.Fatalf("expected guard executor to be called once, got %d", guard.calls)
	}
	if got, want := guard.lastReq.Step.StepRef, xplan.QuoteReadinessGuardStepRef; got != want {
		t.Fatalf("step_ref mismatch: got=%q want=%q", got, want)
	}
}

func TestExecute_UnsupportedStep(t *testing.T) {
	registry := New(Dependencies{})

	_, err := registry.Execute(context.Background(), ExecuteInput{
		Payment:       &agg.Payment{PaymentRef: "p1"},
		Step:          xplan.Step{StepRef: "s1", StepCode: "bad.send", Action: discovery.RailOperationSend, Rail: discovery.RailLedger},
		StepExecution: agg.StepExecution{StepRef: "s1", StepCode: "bad.send", Attempt: 1},
	})
	if !errors.Is(err, ErrUnsupportedStep) {
		t.Fatalf("expected ErrUnsupportedStep, got %v", err)
	}
	if err == nil {
		t.Fatal("expected non-nil error")
	}
	msg := err.Error()
	for _, token := range []string{
		"step_ref=s1",
		"step_code=bad.send",
		"step_kind=UNSPECIFIED",
		"action=SEND",
		"rail=LEDGER",
	} {
		if !strings.Contains(msg, token) {
			t.Fatalf("expected error message to include %q, got %q", token, msg)
		}
	}
}

func TestExecute_UnsupportedProviderSettlementSend(t *testing.T) {
	registry := New(Dependencies{})

	_, err := registry.Execute(context.Background(), ExecuteInput{
		Payment:       &agg.Payment{PaymentRef: "p1"},
		Step:          xplan.Step{StepRef: "s1", StepCode: "provider.send", Action: discovery.RailOperationSend, Rail: discovery.RailProviderSettlement},
		StepExecution: agg.StepExecution{StepRef: "s1", StepCode: "provider.send", Attempt: 1},
	})
	if !errors.Is(err, ErrUnsupportedStep) {
		t.Fatalf("expected ErrUnsupportedStep, got %v", err)
	}
}

func TestExecute_UnsupportedLedgerFee(t *testing.T) {
	registry := New(Dependencies{})

	_, err := registry.Execute(context.Background(), ExecuteInput{
		Payment:       &agg.Payment{PaymentRef: "p1"},
		Step:          xplan.Step{StepRef: "s1", StepCode: "ledger.fee", Action: discovery.RailOperationFee, Rail: discovery.RailLedger},
		StepExecution: agg.StepExecution{StepRef: "s1", StepCode: "ledger.fee", Attempt: 1},
	})
	if !errors.Is(err, ErrUnsupportedStep) {
		t.Fatalf("expected ErrUnsupportedStep, got %v", err)
	}
}

func TestExecute_MissingExecutor(t *testing.T) {
	registry := New(Dependencies{})

	_, err := registry.Execute(context.Background(), ExecuteInput{
		Payment:       &agg.Payment{PaymentRef: "p1"},
		Step:          xplan.Step{StepRef: "s1", StepCode: "crypto.send", Action: discovery.RailOperationSend, Rail: discovery.RailCrypto},
		StepExecution: agg.StepExecution{StepRef: "s1", StepCode: "crypto.send", Attempt: 1},
	})
	if !errors.Is(err, ErrMissingExecutor) {
		t.Fatalf("expected ErrMissingExecutor, got %v", err)
	}
}

func TestExecute_ValidationErrors(t *testing.T) {
	ledger := &fakeLedgerExecutor{}
	registry := New(Dependencies{Ledger: ledger})

	tests := []struct {
		name string
		in   ExecuteInput
	}{
		{
			name: "missing payment",
			in: ExecuteInput{
				Step: xplan.Step{StepRef: "s1", Action: discovery.RailOperationDebit},
			},
		},
		{
			name: "missing step ref",
			in: ExecuteInput{
				Payment: &agg.Payment{},
				Step:    xplan.Step{StepRef: " ", Action: discovery.RailOperationDebit},
			},
		},
		{
			name: "mismatched step execution ref",
			in: ExecuteInput{
				Payment:       &agg.Payment{},
				Step:          xplan.Step{StepRef: "s1", StepCode: "s1", Action: discovery.RailOperationDebit},
				StepExecution: agg.StepExecution{StepRef: "s2"},
			},
		},
	}

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

type fakeLedgerExecutor struct {
	calls   int
	lastReq StepRequest
}

func (f *fakeLedgerExecutor) ExecuteLedger(_ context.Context, req StepRequest) (*ExecuteOutput, error) {
	f.calls++
	f.lastReq = req
	return &ExecuteOutput{
		StepExecution: req.StepExecution,
		Async:         false,
	}, nil
}

type fakeCryptoExecutor struct {
	calls   int
	lastReq StepRequest
}

func (f *fakeCryptoExecutor) ExecuteCrypto(_ context.Context, req StepRequest) (*ExecuteOutput, error) {
	f.calls++
	f.lastReq = req
	return &ExecuteOutput{
		StepExecution: req.StepExecution,
		Async:         true,
	}, nil
}

type fakeProviderSettlementExecutor struct {
	calls   int
	lastReq StepRequest
}

func (f *fakeProviderSettlementExecutor) ExecuteProviderSettlement(_ context.Context, req StepRequest) (*ExecuteOutput, error) {
	f.calls++
	f.lastReq = req
	return &ExecuteOutput{
		StepExecution: req.StepExecution,
		Async:         true,
	}, nil
}

type fakeCardPayoutExecutor struct {
	calls   int
	lastReq StepRequest
}

func (f *fakeCardPayoutExecutor) ExecuteCardPayout(_ context.Context, req StepRequest) (*ExecuteOutput, error) {
	f.calls++
	f.lastReq = req
	return &ExecuteOutput{
		StepExecution: req.StepExecution,
		Async:         true,
	}, nil
}

type fakeObserveConfirmExecutor struct {
	calls   int
	lastReq StepRequest
}

func (f *fakeObserveConfirmExecutor) ExecuteObserveConfirm(_ context.Context, req StepRequest) (*ExecuteOutput, error) {
	f.calls++
	f.lastReq = req
	return &ExecuteOutput{
		StepExecution: req.StepExecution,
		Async:         true,
	}, nil
}

type fakeGuardExecutor struct {
	calls   int
	lastReq StepRequest
}

func (f *fakeGuardExecutor) ExecuteGuard(_ context.Context, req StepRequest) (*ExecuteOutput, error) {
	f.calls++
	f.lastReq = req
	return &ExecuteOutput{
		StepExecution: req.StepExecution,
		Async:         false,
	}, nil
}
