package xplan

import (
	"github.com/tech/sendico/pkg/discovery"
	"testing"

	"github.com/tech/sendico/payments/storage/model"
	paymenttypes "github.com/tech/sendico/pkg/payments/types"
)

func TestCompile_ExternalToExternal_BridgeExpansion(t *testing.T) {
	compiler := New()

	graph, err := compiler.Compile(Input{
		IntentSnapshot: testIntent(model.PaymentKindPayout),
		QuoteSnapshot: &model.PaymentQuoteSnapshot{
			Route: &paymenttypes.QuoteRouteSpecification{
				RouteRef: "route-1",
				Hops: []*paymenttypes.QuoteRouteHop{
					{
						Index:      10,
						Rail:       "CRYPTO",
						Gateway:    "gw-crypto",
						InstanceID: "crypto-1",
						Role:       paymenttypes.QuoteRouteHopRoleSource,
					},
					{
						Index:      20,
						Rail:       "CARD",
						Gateway:    "gw-card",
						InstanceID: "card-1",
						Role:       paymenttypes.QuoteRouteHopRoleDestination,
					},
				},
			},
		},
	})
	if err != nil {
		t.Fatalf("Compile returned error: %v", err)
	}
	if graph == nil {
		t.Fatal("expected graph")
	}
	if got, want := graph.RouteRef, "route-1"; got != want {
		t.Fatalf("route_ref mismatch: got=%q want=%q", got, want)
	}
	if len(graph.Steps) != 9 {
		t.Fatalf("expected 9 steps, got %d", len(graph.Steps))
	}

	assertStep(t, graph.Steps[0], "hop.10.crypto.send", discovery.RailOperationSend, discovery.RailCrypto, model.ReportVisibilityBackoffice)
	assertStep(t, graph.Steps[1], "hop.10.crypto.observe", discovery.RailOperationObserveConfirm, discovery.RailCrypto, model.ReportVisibilityBackoffice)
	assertStep(t, graph.Steps[2], "edge.10_20.ledger.credit", discovery.RailOperationExternalCredit, discovery.RailLedger, model.ReportVisibilityHidden)
	assertStep(t, graph.Steps[3], "edge.10_20.ledger.block", discovery.RailOperationBlock, discovery.RailLedger, model.ReportVisibilityHidden)
	assertStep(t, graph.Steps[4], "hop.20.card_payout.send", discovery.RailOperationSend, discovery.RailCardPayout, model.ReportVisibilityUser)
	assertStep(t, graph.Steps[5], "hop.20.card_payout.observe", discovery.RailOperationObserveConfirm, discovery.RailCardPayout, model.ReportVisibilityUser)
	assertStep(t, graph.Steps[6], "edge.10_20.ledger.debit", discovery.RailOperationExternalDebit, discovery.RailLedger, model.ReportVisibilityHidden)
	assertStep(t, graph.Steps[7], "edge.10_20.ledger.release", discovery.RailOperationRelease, discovery.RailLedger, model.ReportVisibilityHidden)
	assertStep(t, graph.Steps[8], "edge.10_20.ledger.release", discovery.RailOperationRelease, discovery.RailLedger, model.ReportVisibilityHidden)

	if got, want := graph.Steps[1].DependsOn, []string{graph.Steps[0].StepRef}; !equalStringSlice(got, want) {
		t.Fatalf("step[1] deps mismatch: got=%v want=%v", got, want)
	}
	if got, want := graph.Steps[2].DependsOn, []string{graph.Steps[1].StepRef}; !equalStringSlice(got, want) {
		t.Fatalf("step[2] deps mismatch: got=%v want=%v", got, want)
	}
	if got, want := graph.Steps[3].DependsOn, []string{graph.Steps[2].StepRef}; !equalStringSlice(got, want) {
		t.Fatalf("step[3] deps mismatch: got=%v want=%v", got, want)
	}
	if got, want := graph.Steps[4].DependsOn, []string{graph.Steps[3].StepRef}; !equalStringSlice(got, want) {
		t.Fatalf("step[4] deps mismatch: got=%v want=%v", got, want)
	}
	if got, want := graph.Steps[5].DependsOn, []string{graph.Steps[4].StepRef}; !equalStringSlice(got, want) {
		t.Fatalf("step[5] deps mismatch: got=%v want=%v", got, want)
	}

	if graph.Steps[6].CommitPolicy != model.CommitPolicyAfterSuccess {
		t.Fatalf("expected debit commit policy AFTER_SUCCESS, got %q", graph.Steps[6].CommitPolicy)
	}
	if graph.Steps[7].CommitPolicy != model.CommitPolicyAfterFailure {
		t.Fatalf("expected send-failure release commit policy AFTER_FAILURE, got %q", graph.Steps[7].CommitPolicy)
	}
	if graph.Steps[8].CommitPolicy != model.CommitPolicyAfterFailure {
		t.Fatalf("expected observe-failure release commit policy AFTER_FAILURE, got %q", graph.Steps[8].CommitPolicy)
	}
	if got, want := graph.Steps[6].CommitAfter, []string{graph.Steps[5].StepRef}; !equalStringSlice(got, want) {
		t.Fatalf("debit commit_after mismatch: got=%v want=%v", got, want)
	}
	if got, want := graph.Steps[7].CommitAfter, []string{graph.Steps[4].StepRef}; !equalStringSlice(got, want) {
		t.Fatalf("send-failure release commit_after mismatch: got=%v want=%v", got, want)
	}
	if got, want := graph.Steps[8].CommitAfter, []string{graph.Steps[5].StepRef}; !equalStringSlice(got, want) {
		t.Fatalf("observe-failure release commit_after mismatch: got=%v want=%v", got, want)
	}
	if got, want := graph.Steps[6].Metadata["mode"], "finalize_debit"; got != want {
		t.Fatalf("expected debit mode %q, got %q", want, got)
	}
	if got, want := graph.Steps[7].Metadata["mode"], "unlock_hold"; got != want {
		t.Fatalf("expected send-failure release mode %q, got %q", want, got)
	}
	if got, want := graph.Steps[8].Metadata["mode"], "unlock_hold"; got != want {
		t.Fatalf("expected observe-failure release mode %q, got %q", want, got)
	}
}

func TestCompile_ExternalToExternal_WithWalletFee_InsertsFeeStep(t *testing.T) {
	compiler := New()

	graph, err := compiler.Compile(Input{
		IntentSnapshot: testIntent(model.PaymentKindPayout),
		QuoteSnapshot: &model.PaymentQuoteSnapshot{
			DebitAmount: &paymenttypes.Money{Amount: "5.842042", Currency: "USDT"},
			FeeLines: []*paymenttypes.FeeLine{
				{
					Money: &paymenttypes.Money{Amount: "0.42", Currency: "USDT"},
					Side:  paymenttypes.EntrySideDebit,
					Meta:  map[string]string{"fee_target": "wallet"},
				},
			},
			Route: &paymenttypes.QuoteRouteSpecification{
				RouteRef: "route-1",
				Hops: []*paymenttypes.QuoteRouteHop{
					{
						Index:      10,
						Rail:       "CRYPTO",
						Gateway:    "gw-crypto",
						InstanceID: "crypto-1",
						Role:       paymenttypes.QuoteRouteHopRoleSource,
					},
					{
						Index:      20,
						Rail:       "CARD",
						Gateway:    "gw-card",
						InstanceID: "card-1",
						Role:       paymenttypes.QuoteRouteHopRoleDestination,
					},
				},
			},
		},
	})
	if err != nil {
		t.Fatalf("Compile returned error: %v", err)
	}
	if graph == nil {
		t.Fatal("expected graph")
	}
	if got, want := len(graph.Steps), 10; got != want {
		t.Fatalf("expected 10 steps, got %d", got)
	}

	assertStep(t, graph.Steps[0], "hop.10.crypto.send", discovery.RailOperationSend, discovery.RailCrypto, model.ReportVisibilityBackoffice)
	assertStep(t, graph.Steps[1], "hop.10.crypto.fee", discovery.RailOperationFee, discovery.RailCrypto, model.ReportVisibilityHidden)
	assertStep(t, graph.Steps[2], "hop.10.crypto.observe", discovery.RailOperationObserveConfirm, discovery.RailCrypto, model.ReportVisibilityBackoffice)
	assertStep(t, graph.Steps[3], "edge.10_20.ledger.credit", discovery.RailOperationExternalCredit, discovery.RailLedger, model.ReportVisibilityHidden)
	assertStep(t, graph.Steps[4], "edge.10_20.ledger.block", discovery.RailOperationBlock, discovery.RailLedger, model.ReportVisibilityHidden)
	assertStep(t, graph.Steps[5], "hop.20.card_payout.send", discovery.RailOperationSend, discovery.RailCardPayout, model.ReportVisibilityUser)
	assertStep(t, graph.Steps[6], "hop.20.card_payout.observe", discovery.RailOperationObserveConfirm, discovery.RailCardPayout, model.ReportVisibilityUser)
	assertStep(t, graph.Steps[7], "edge.10_20.ledger.debit", discovery.RailOperationExternalDebit, discovery.RailLedger, model.ReportVisibilityHidden)
	assertStep(t, graph.Steps[8], "edge.10_20.ledger.release", discovery.RailOperationRelease, discovery.RailLedger, model.ReportVisibilityHidden)
	assertStep(t, graph.Steps[9], "edge.10_20.ledger.release", discovery.RailOperationRelease, discovery.RailLedger, model.ReportVisibilityHidden)

	if got, want := graph.Steps[1].DependsOn, []string{graph.Steps[0].StepRef}; !equalStringSlice(got, want) {
		t.Fatalf("fee deps mismatch: got=%v want=%v", got, want)
	}
	if got, want := graph.Steps[2].DependsOn, []string{graph.Steps[1].StepRef}; !equalStringSlice(got, want) {
		t.Fatalf("observe deps mismatch: got=%v want=%v", got, want)
	}
	if got, want := graph.Steps[3].DependsOn, []string{graph.Steps[2].StepRef}; !equalStringSlice(got, want) {
		t.Fatalf("credit deps mismatch: got=%v want=%v", got, want)
	}
}

func TestCompile_ExternalToExternal_IncludesNonWalletFeeLines(t *testing.T) {
	compiler := New()

	graph, err := compiler.Compile(Input{
		IntentSnapshot: testIntent(model.PaymentKindPayout),
		QuoteSnapshot: &model.PaymentQuoteSnapshot{
			DebitAmount: &paymenttypes.Money{Amount: "5.842042", Currency: "USDT"},
			FeeLines: []*paymenttypes.FeeLine{
				{
					Money: &paymenttypes.Money{Amount: "0.42", Currency: "USDT"},
					Side:  paymenttypes.EntrySideDebit,
					Meta:  map[string]string{"fee_target": "ledger"},
				},
			},
			Route: &paymenttypes.QuoteRouteSpecification{
				Hops: []*paymenttypes.QuoteRouteHop{
					{Index: 10, Rail: "CRYPTO", Role: paymenttypes.QuoteRouteHopRoleSource},
					{Index: 20, Rail: "CARD", Role: paymenttypes.QuoteRouteHopRoleDestination},
				},
			},
		},
	})
	if err != nil {
		t.Fatalf("Compile returned error: %v", err)
	}
	if got, want := len(graph.Steps), 10; got != want {
		t.Fatalf("expected 10 steps, got %d", got)
	}
	feeCount := 0
	for i := range graph.Steps {
		if graph.Steps[i].Action == discovery.RailOperationFee {
			feeCount++
		}
	}
	if got, want := feeCount, 1; got != want {
		t.Fatalf("fee steps mismatch: got=%d want=%d", got, want)
	}
}

func TestCompile_InternalToExternal_UsesHoldAndSettlementBranches(t *testing.T) {
	compiler := New()

	graph, err := compiler.Compile(Input{
		IntentSnapshot: testIntent(model.PaymentKindPayout),
		QuoteSnapshot: &model.PaymentQuoteSnapshot{
			Route: &paymenttypes.QuoteRouteSpecification{
				Hops: []*paymenttypes.QuoteRouteHop{
					{Index: 10, Rail: "LEDGER", Role: paymenttypes.QuoteRouteHopRoleSource},
					{Index: 20, Rail: "CARD", Gateway: "gw-card", Role: paymenttypes.QuoteRouteHopRoleDestination},
				},
			},
		},
	})
	if err != nil {
		t.Fatalf("Compile returned error: %v", err)
	}
	if len(graph.Steps) != 6 {
		t.Fatalf("expected 6 steps, got %d", len(graph.Steps))
	}
	assertStep(t, graph.Steps[0], "edge.10_20.ledger.block", discovery.RailOperationBlock, discovery.RailLedger, model.ReportVisibilityHidden)
	assertStep(t, graph.Steps[1], "hop.20.card_payout.send", discovery.RailOperationSend, discovery.RailCardPayout, model.ReportVisibilityUser)
	assertStep(t, graph.Steps[2], "hop.20.card_payout.observe", discovery.RailOperationObserveConfirm, discovery.RailCardPayout, model.ReportVisibilityUser)
	assertStep(t, graph.Steps[3], "edge.10_20.ledger.debit", discovery.RailOperationExternalDebit, discovery.RailLedger, model.ReportVisibilityHidden)
	assertStep(t, graph.Steps[4], "edge.10_20.ledger.release", discovery.RailOperationRelease, discovery.RailLedger, model.ReportVisibilityHidden)
	assertStep(t, graph.Steps[5], "edge.10_20.ledger.release", discovery.RailOperationRelease, discovery.RailLedger, model.ReportVisibilityHidden)
	if got, want := graph.Steps[4].CommitAfter, []string{graph.Steps[1].StepRef}; !equalStringSlice(got, want) {
		t.Fatalf("send-failure release commit_after mismatch: got=%v want=%v", got, want)
	}
	if got, want := graph.Steps[5].CommitAfter, []string{graph.Steps[2].StepRef}; !equalStringSlice(got, want) {
		t.Fatalf("observe-failure release commit_after mismatch: got=%v want=%v", got, want)
	}
}

func TestCompile_InternalToExternal_WithWalletFee_InsertsFeeStep(t *testing.T) {
	compiler := New()

	graph, err := compiler.Compile(Input{
		IntentSnapshot: testIntent(model.PaymentKindPayout),
		QuoteSnapshot: &model.PaymentQuoteSnapshot{
			DebitAmount: &paymenttypes.Money{Amount: "100", Currency: "USDT"},
			FeeLines: []*paymenttypes.FeeLine{
				{
					Money: &paymenttypes.Money{Amount: "1", Currency: "USDT"},
					Side:  paymenttypes.EntrySideDebit,
					Meta:  map[string]string{"fee_target": "wallet"},
				},
			},
			Route: &paymenttypes.QuoteRouteSpecification{
				Hops: []*paymenttypes.QuoteRouteHop{
					{Index: 10, Rail: "LEDGER", Role: paymenttypes.QuoteRouteHopRoleSource},
					{Index: 20, Rail: "CARD", Gateway: "gw-card", Role: paymenttypes.QuoteRouteHopRoleDestination},
				},
			},
		},
	})
	if err != nil {
		t.Fatalf("Compile returned error: %v", err)
	}
	if len(graph.Steps) != 7 {
		t.Fatalf("expected 7 steps, got %d", len(graph.Steps))
	}

	assertStep(t, graph.Steps[0], "edge.10_20.ledger.block", discovery.RailOperationBlock, discovery.RailLedger, model.ReportVisibilityHidden)
	assertStep(t, graph.Steps[1], "hop.10.ledger.fee", discovery.RailOperationFee, discovery.RailLedger, model.ReportVisibilityHidden)
	assertStep(t, graph.Steps[2], "hop.20.card_payout.send", discovery.RailOperationSend, discovery.RailCardPayout, model.ReportVisibilityUser)
	assertStep(t, graph.Steps[3], "hop.20.card_payout.observe", discovery.RailOperationObserveConfirm, discovery.RailCardPayout, model.ReportVisibilityUser)
	assertStep(t, graph.Steps[4], "edge.10_20.ledger.debit", discovery.RailOperationExternalDebit, discovery.RailLedger, model.ReportVisibilityHidden)
	assertStep(t, graph.Steps[5], "edge.10_20.ledger.release", discovery.RailOperationRelease, discovery.RailLedger, model.ReportVisibilityHidden)
	assertStep(t, graph.Steps[6], "edge.10_20.ledger.release", discovery.RailOperationRelease, discovery.RailLedger, model.ReportVisibilityHidden)

	if got, want := graph.Steps[1].DependsOn, []string{graph.Steps[0].StepRef}; !equalStringSlice(got, want) {
		t.Fatalf("fee deps mismatch: got=%v want=%v", got, want)
	}
	if got, want := graph.Steps[2].DependsOn, []string{graph.Steps[1].StepRef}; !equalStringSlice(got, want) {
		t.Fatalf("send deps mismatch: got=%v want=%v", got, want)
	}
}

func TestCompile_ExternalToInternal_UsesCreditAfterObserve(t *testing.T) {
	compiler := New()

	graph, err := compiler.Compile(Input{
		IntentSnapshot: testIntent(model.PaymentKindInternalTransfer),
		QuoteSnapshot: &model.PaymentQuoteSnapshot{
			Route: &paymenttypes.QuoteRouteSpecification{
				Hops: []*paymenttypes.QuoteRouteHop{
					{Index: 10, Rail: "CRYPTO", Role: paymenttypes.QuoteRouteHopRoleSource},
					{Index: 20, Rail: "LEDGER", Role: paymenttypes.QuoteRouteHopRoleDestination},
				},
			},
		},
	})
	if err != nil {
		t.Fatalf("Compile returned error: %v", err)
	}
	if len(graph.Steps) != 3 {
		t.Fatalf("expected 3 steps, got %d", len(graph.Steps))
	}
	assertStep(t, graph.Steps[0], "hop.10.crypto.send", discovery.RailOperationSend, discovery.RailCrypto, model.ReportVisibilityBackoffice)
	assertStep(t, graph.Steps[1], "hop.10.crypto.observe", discovery.RailOperationObserveConfirm, discovery.RailCrypto, model.ReportVisibilityBackoffice)
	assertStep(t, graph.Steps[2], "edge.10_20.ledger.credit", discovery.RailOperationExternalCredit, discovery.RailLedger, model.ReportVisibilityHidden)
}

func TestCompile_ExternalViaSettlement_UsesFXConvertOnSettlementHop(t *testing.T) {
	compiler := New()

	graph, err := compiler.Compile(Input{
		IntentSnapshot: testIntent(model.PaymentKindPayout),
		QuoteSnapshot: &model.PaymentQuoteSnapshot{
			Route: &paymenttypes.QuoteRouteSpecification{
				Hops: []*paymenttypes.QuoteRouteHop{
					{
						Index:      10,
						Rail:       "CRYPTO",
						Gateway:    "gw-crypto",
						InstanceID: "crypto-1",
						Role:       paymenttypes.QuoteRouteHopRoleSource,
					},
					{
						Index:      20,
						Rail:       "SETTLEMENT",
						Gateway:    "gw-settlement",
						InstanceID: "settlement-1",
						Role:       paymenttypes.QuoteRouteHopRoleTransit,
					},
					{
						Index:      30,
						Rail:       "CARD",
						Gateway:    "gw-card",
						InstanceID: "card-1",
						Role:       paymenttypes.QuoteRouteHopRoleDestination,
					},
				},
			},
		},
	})
	if err != nil {
		t.Fatalf("Compile returned error: %v", err)
	}

	convertCount := 0
	sendCount := 0
	for _, step := range graph.Steps {
		if step.Rail != discovery.RailProviderSettlement {
			continue
		}
		if step.Action == discovery.RailOperationFXConvert {
			convertCount++
		}
		if step.Action == discovery.RailOperationSend {
			sendCount++
		}
	}
	if convertCount == 0 {
		t.Fatalf("expected at least one settlement FX_CONVERT step")
	}
	if sendCount != 0 {
		t.Fatalf("expected no settlement SEND steps, got %d", sendCount)
	}
}

func TestCompile_InternalToInternal_UsesMove(t *testing.T) {
	compiler := New()

	graph, err := compiler.Compile(Input{
		IntentSnapshot: testIntent(model.PaymentKindInternalTransfer),
		QuoteSnapshot: &model.PaymentQuoteSnapshot{
			Route: &paymenttypes.QuoteRouteSpecification{
				Hops: []*paymenttypes.QuoteRouteHop{
					{Index: 10, Rail: "LEDGER", Role: paymenttypes.QuoteRouteHopRoleSource},
					{Index: 20, Rail: "LEDGER", Role: paymenttypes.QuoteRouteHopRoleDestination},
				},
			},
		},
	})
	if err != nil {
		t.Fatalf("Compile returned error: %v", err)
	}
	if len(graph.Steps) != 1 {
		t.Fatalf("expected 1 step, got %d", len(graph.Steps))
	}
	assertStep(t, graph.Steps[0], "edge.10_20.ledger.move", discovery.RailOperationMove, discovery.RailLedger, model.ReportVisibilityHidden)
}

func TestCompile_GuardsArePrepended(t *testing.T) {
	compiler := New()

	graph, err := compiler.Compile(Input{
		IntentSnapshot: testIntent(model.PaymentKindPayout),
		QuoteSnapshot: &model.PaymentQuoteSnapshot{
			Route: &paymenttypes.QuoteRouteSpecification{
				Hops: []*paymenttypes.QuoteRouteHop{
					{Index: 10, Rail: "LEDGER", Role: paymenttypes.QuoteRouteHopRoleSource},
					{Index: 20, Rail: "CARD", Role: paymenttypes.QuoteRouteHopRoleDestination},
				},
			},
			ExecutionConditions: &paymenttypes.QuoteExecutionConditions{
				LiquidityCheckRequiredAtExecution: true,
				PrefundingRequired:                true,
			},
		},
	})
	if err != nil {
		t.Fatalf("Compile returned error: %v", err)
	}
	if len(graph.Steps) != 8 {
		t.Fatalf("expected 8 steps, got %d", len(graph.Steps))
	}
	if graph.Steps[0].Kind != StepKindLiquidityCheck {
		t.Fatalf("expected first guard liquidity_check, got %q", graph.Steps[0].Kind)
	}
	if graph.Steps[1].Kind != StepKindPrefunding {
		t.Fatalf("expected second guard prefunding, got %q", graph.Steps[1].Kind)
	}
	if got, want := graph.Steps[1].DependsOn, []string{graph.Steps[0].StepRef}; !equalStringSlice(got, want) {
		t.Fatalf("prefunding dependency mismatch: got=%v want=%v", got, want)
	}
	if got, want := graph.Steps[2].DependsOn, []string{graph.Steps[1].StepRef}; !equalStringSlice(got, want) {
		t.Fatalf("first execution step dependency mismatch: got=%v want=%v", got, want)
	}
}

func TestCompile_SingleExternalFallback(t *testing.T) {
	compiler := New()

	graph, err := compiler.Compile(Input{
		IntentSnapshot: testIntent(model.PaymentKindPayout),
		QuoteSnapshot: &model.PaymentQuoteSnapshot{
			Route: &paymenttypes.QuoteRouteSpecification{
				RouteRef: "route-summary",
				Rail:     "CARD",
				Provider: "gw-card",
				Network:  "visa",
			},
		},
	})
	if err != nil {
		t.Fatalf("Compile returned error: %v", err)
	}
	if len(graph.Steps) != 2 {
		t.Fatalf("expected 2 steps, got %d", len(graph.Steps))
	}
	assertStep(t, graph.Steps[0], "hop.0.card_payout.send", discovery.RailOperationSend, discovery.RailCardPayout, model.ReportVisibilityUser)
	assertStep(t, graph.Steps[1], "hop.0.card_payout.observe", discovery.RailOperationObserveConfirm, discovery.RailCardPayout, model.ReportVisibilityUser)
	if got, want := graph.Steps[1].DependsOn, []string{graph.Steps[0].StepRef}; !equalStringSlice(got, want) {
		t.Fatalf("observe dependency mismatch: got=%v want=%v", got, want)
	}
}
