package orchestrator

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

	mntxclient "github.com/tech/sendico/gateway/mntx/client"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/batchmeta"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/erecon"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/sexec"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/xplan"
	"github.com/tech/sendico/payments/storage/model"
	pm "github.com/tech/sendico/pkg/model"
	paymenttypes "github.com/tech/sendico/pkg/payments/types"
	mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
	"go.mongodb.org/mongo-driver/v2/bson"
)

func TestGatewayCardPayoutExecutor_ExecuteCardPayout_SubmitsCardPayout(t *testing.T) {
	orgID := bson.NewObjectID()

	var payoutReq *mntxv1.CardPayoutRequest
	var dialAddress string
	executor := &gatewayCardPayoutExecutor{
		gatewayRegistry: &fakeGatewayRegistry{
			items: []*model.GatewayInstanceDescriptor{
				{
					ID:         paymenttypes.DefaultCardsGatewayID,
					InstanceID: paymenttypes.DefaultCardsGatewayID,
					Rail:       discovery.RailCardPayout,
					InvokeURI:  "grpc://mntx-gateway:50051",
					IsEnabled:  true,
				},
			},
		},
		dialClient: func(_ context.Context, address string) (mntxclient.Client, error) {
			dialAddress = address
			return &mntxclient.Fake{
				CreateCardPayoutFn: func(_ context.Context, req *mntxv1.CardPayoutRequest) (*mntxv1.CardPayoutResponse, error) {
					payoutReq = req
					return &mntxv1.CardPayoutResponse{
						Payout: &mntxv1.CardPayoutState{
							PayoutId: "payout-remote-1",
						},
					}, nil
				},
			}, nil
		},
	}

	req := sexec.StepRequest{
		Payment: &agg.Payment{
			OrganizationBoundBase: pm.OrganizationBoundBase{OrganizationRef: orgID},
			PaymentRef:            "payment-1",
			IdempotencyKey:        "idem-1",
			QuotationRef:          "quote-1",
			IntentSnapshot: model.PaymentIntent{
				Ref:     "intent-1",
				Comment: "invoice-1",
				Destination: model.PaymentEndpoint{
					Type: model.EndpointTypeCard,
					Card: &model.CardEndpoint{
						Pan:               "2200700142860161",
						Cardholder:        "Stephan",
						CardholderSurname: "Deshevikh",
						ExpMonth:          3,
						ExpYear:           2030,
					},
				},
				Customer: &model.Customer{
					ID:        "cust-1",
					FirstName: "Stephan",
					LastName:  "Deshevikh",
					IP:        "198.51.100.10",
				},
				Amount: &paymenttypes.Money{
					Amount:   "1.000000",
					Currency: "USDT",
				},
				Attributes: map[string]string{
					"initiator_ref": "user-123",
				},
			},
			QuoteSnapshot: &model.PaymentQuoteSnapshot{
				ExpectedSettlementAmount: &paymenttypes.Money{
					Amount:   "76.50",
					Currency: "RUB",
				},
				QuoteRef: "quote-1",
			},
		},
		Step: xplan.Step{
			StepRef:    "hop_4_card_payout_send",
			StepCode:   "hop.4.card_payout.send",
			Action:     discovery.RailOperationSend,
			Rail:       discovery.RailCardPayout,
			Gateway:    paymenttypes.DefaultCardsGatewayID,
			InstanceID: paymenttypes.DefaultCardsGatewayID,
			HopRole:    paymenttypes.QuoteRouteHopRoleDestination,
		},
		StepExecution: agg.StepExecution{
			StepRef:        "hop_4_card_payout_send",
			StepCode:       "hop.4.card_payout.send",
			Attempt:        1,
			ConvertedMoney: &paymenttypes.Money{Amount: "1.00", Currency: "USD"},
		},
	}

	out, err := executor.ExecuteCardPayout(context.Background(), req)
	if err != nil {
		t.Fatalf("ExecuteCardPayout returned error: %v", err)
	}
	if out == nil {
		t.Fatal("expected output")
	}
	if out.StepExecution.State != agg.StepStateCompleted {
		t.Fatalf("expected completed state, got=%q", out.StepExecution.State)
	}
	if out.StepExecution.ExecutedMoney == nil {
		t.Fatal("expected executed money to be recorded")
	}
	if got, want := out.StepExecution.ExecutedMoney.Amount, "76.50"; got != want {
		t.Fatalf("executed money amount mismatch: got=%q want=%q", got, want)
	}
	if got, want := out.StepExecution.ExecutedMoney.Currency, "RUB"; got != want {
		t.Fatalf("executed money currency mismatch: got=%q want=%q", got, want)
	}
	if got := out.StepExecution.ConvertedMoney; got != nil {
		t.Fatalf("expected converted money to be cleared for card payout step, got=%+v", got)
	}
	if payoutReq == nil {
		t.Fatal("expected payout request to be submitted")
	}
	if got, want := dialAddress, "mntx-gateway:50051"; got != want {
		t.Fatalf("dial address mismatch: got=%q want=%q", got, want)
	}
	if got, want := payoutReq.GetPayoutId(), ""; got != want {
		t.Fatalf("payout_id mismatch: got=%q want=%q", got, want)
	}
	if got, want := payoutReq.GetOperationRef(), "payment-1:hop_4_card_payout_send"; got != want {
		t.Fatalf("operation_ref mismatch: got=%q want=%q", got, want)
	}
	if got, want := payoutReq.GetIdempotencyKey(), "idem-1:hop_4_card_payout_send"; got != want {
		t.Fatalf("idempotency mismatch: got=%q want=%q", got, want)
	}
	if got, want := payoutReq.GetAmountMinor(), int64(7650); got != want {
		t.Fatalf("amount_minor mismatch: got=%d want=%d", got, want)
	}
	if got, want := payoutReq.GetCurrency(), "RUB"; got != want {
		t.Fatalf("currency mismatch: got=%q want=%q", got, want)
	}
	if got, want := payoutReq.GetMetadata()[settlementMetadataQuoteRef], "quote-1"; got != want {
		t.Fatalf("quote_ref metadata mismatch: got=%q want=%q", got, want)
	}
	if got, want := payoutReq.GetMetadata()[settlementMetadataOutgoingLeg], string(discovery.RailCardPayout); got != want {
		t.Fatalf("outgoing_leg metadata mismatch: got=%q want=%q", got, want)
	}
	if got, want := payoutReq.GetComment(), "invoice-1"; got != want {
		t.Fatalf("comment mismatch: got=%q want=%q", got, want)
	}
	if _, ok := payoutReq.GetMetadata()["comment"]; ok {
		t.Fatalf("comment must not be sent in metadata")
	}
	if got, want := out.StepExecution.Comment, "invoice-1"; got != want {
		t.Fatalf("step comment mismatch: got=%q want=%q", got, want)
	}
	if got, want := payoutReq.GetParentPaymentRef(), "payment-1"; got != want {
		t.Fatalf("parent_payment_ref mismatch: got=%q want=%q", got, want)
	}
	if len(out.StepExecution.ExternalRefs) != 3 {
		t.Fatalf("expected 3 external refs, got=%d", len(out.StepExecution.ExternalRefs))
	}
	if out.StepExecution.ExternalRefs[0].Kind != erecon.ExternalRefKindOperation {
		t.Fatalf("expected first external ref to be operation, got=%q", out.StepExecution.ExternalRefs[0].Kind)
	}
	if out.StepExecution.ExternalRefs[1].Kind != erecon.ExternalRefKindTransfer {
		t.Fatalf("expected second external ref to be transfer, got=%q", out.StepExecution.ExternalRefs[1].Kind)
	}
	if out.StepExecution.ExternalRefs[2].Kind != erecon.ExternalRefKindCardPayout {
		t.Fatalf("expected third external ref to be card payout, got=%q", out.StepExecution.ExternalRefs[2].Kind)
	}
	if got, want := out.StepExecution.ExternalRefs[1].Ref, "payout-remote-1"; got != want {
		t.Fatalf("transfer_ref mismatch: got=%q want=%q", got, want)
	}
}

func TestGatewayCardPayoutExecutor_ExecuteCardPayout_UsesPlannedMoney(t *testing.T) {
	orgID := bson.NewObjectID()

	var payoutReq *mntxv1.CardPayoutRequest
	executor := &gatewayCardPayoutExecutor{
		gatewayRegistry: &fakeGatewayRegistry{
			items: []*model.GatewayInstanceDescriptor{
				{
					ID:         paymenttypes.DefaultCardsGatewayID,
					InstanceID: paymenttypes.DefaultCardsGatewayID,
					Rail:       discovery.RailCardPayout,
					InvokeURI:  "grpc://mntx-gateway:50051",
					IsEnabled:  true,
				},
			},
		},
		dialClient: func(_ context.Context, _ string) (mntxclient.Client, error) {
			return &mntxclient.Fake{
				CreateCardPayoutFn: func(_ context.Context, req *mntxv1.CardPayoutRequest) (*mntxv1.CardPayoutResponse, error) {
					payoutReq = req
					return &mntxv1.CardPayoutResponse{
						Payout: &mntxv1.CardPayoutState{
							PayoutId: "payout-planned",
						},
					}, nil
				},
			}, nil
		},
	}

	_, err := executor.ExecuteCardPayout(context.Background(), sexec.StepRequest{
		Payment: &agg.Payment{
			OrganizationBoundBase: pm.OrganizationBoundBase{OrganizationRef: orgID},
			PaymentRef:            "payment-planned",
			IdempotencyKey:        "idem-planned",
			IntentSnapshot: model.PaymentIntent{
				Ref: "intent-planned",
				Destination: model.PaymentEndpoint{
					Type: model.EndpointTypeCard,
					Card: &model.CardEndpoint{
						Pan:      "2200700142860161",
						ExpMonth: 3,
						ExpYear:  2030,
					},
				},
				Amount: &paymenttypes.Money{Amount: "1.000000", Currency: "USDT"},
			},
			QuoteSnapshot: &model.PaymentQuoteSnapshot{
				ExpectedSettlementAmount: &paymenttypes.Money{Amount: "76.50", Currency: "RUB"},
			},
		},
		Step: xplan.Step{
			StepRef:    "hop_4_card_payout_send",
			StepCode:   "hop.4.card_payout.send",
			Action:     discovery.RailOperationSend,
			Rail:       discovery.RailCardPayout,
			Gateway:    paymenttypes.DefaultCardsGatewayID,
			InstanceID: paymenttypes.DefaultCardsGatewayID,
		},
		StepExecution: agg.StepExecution{
			StepRef:      "hop_4_card_payout_send",
			StepCode:     "hop.4.card_payout.send",
			Attempt:      1,
			PlannedMoney: &paymenttypes.Money{Amount: "100.00", Currency: "RUB"},
		},
	})
	if err != nil {
		t.Fatalf("ExecuteCardPayout returned error: %v", err)
	}
	if payoutReq == nil {
		t.Fatal("expected payout request to be submitted")
	}
	if got, want := payoutReq.GetAmountMinor(), int64(10000); got != want {
		t.Fatalf("amount_minor mismatch: got=%d want=%d", got, want)
	}
	if got, want := payoutReq.GetCurrency(), "RUB"; got != want {
		t.Fatalf("currency mismatch: got=%q want=%q", got, want)
	}
}

func TestGatewayCardPayoutExecutor_ExecuteCardPayout_UsesStepMetadataOverrides(t *testing.T) {
	orgID := bson.NewObjectID()

	var payoutReq *mntxv1.CardPayoutRequest
	var dialAddress string
	executor := &gatewayCardPayoutExecutor{
		gatewayRegistry: &fakeGatewayRegistry{
			items: []*model.GatewayInstanceDescriptor{
				{
					ID:         paymenttypes.DefaultCardsGatewayID,
					InstanceID: paymenttypes.DefaultCardsGatewayID,
					Rail:       discovery.RailCardPayout,
					InvokeURI:  "grpc://mntx-gateway:50051",
					IsEnabled:  true,
				},
			},
		},
		dialClient: func(_ context.Context, address string) (mntxclient.Client, error) {
			dialAddress = address
			return &mntxclient.Fake{
				CreateCardPayoutFn: func(_ context.Context, req *mntxv1.CardPayoutRequest) (*mntxv1.CardPayoutResponse, error) {
					payoutReq = req
					return &mntxv1.CardPayoutResponse{
						Payout: &mntxv1.CardPayoutState{
							PayoutId: "payout-remote-2",
						},
					}, nil
				},
			}, nil
		},
	}

	req := sexec.StepRequest{
		Payment: &agg.Payment{
			OrganizationBoundBase: pm.OrganizationBoundBase{OrganizationRef: orgID},
			PaymentRef:            "payment-2",
			IdempotencyKey:        "idem-2",
			QuotationRef:          "quote-2",
			IntentSnapshot: model.PaymentIntent{
				Ref: "intent-2",
				Destination: model.PaymentEndpoint{
					Type: model.EndpointTypeCard,
					Card: &model.CardEndpoint{
						Pan:      "2200700142860999",
						ExpMonth: 1,
						ExpYear:  2030,
					},
				},
				Amount: &paymenttypes.Money{
					Amount:   "1.000000",
					Currency: "USDT",
				},
			},
			QuoteSnapshot: &model.PaymentQuoteSnapshot{
				ExpectedSettlementAmount: &paymenttypes.Money{
					Amount:   "76.50",
					Currency: "RUB",
				},
				QuoteRef: "quote-2",
			},
		},
		Step: xplan.Step{
			StepRef:    "hop_4_card_payout_send",
			StepCode:   "hop.4.card_payout.send",
			Action:     discovery.RailOperationSend,
			Rail:       discovery.RailCardPayout,
			Gateway:    paymenttypes.DefaultCardsGatewayID,
			InstanceID: paymenttypes.DefaultCardsGatewayID,
			Comment:    "batch-note-2",
			Metadata: map[string]string{
				batchmeta.MetaPayoutTargetRef: "recipient-2",
				batchmeta.MetaAmount:          "150",
				batchmeta.MetaCurrency:        "RUB",
				batchmeta.MetaCardPan:         "2200700142860162",
				batchmeta.MetaCardExpMonth:    "4",
				batchmeta.MetaCardExpYear:     "2030",
			},
			HopRole: paymenttypes.QuoteRouteHopRoleDestination,
		},
		StepExecution: agg.StepExecution{
			StepRef:  "hop_4_card_payout_send",
			StepCode: "hop.4.card_payout.send",
			Attempt:  1,
		},
	}

	out, err := executor.ExecuteCardPayout(context.Background(), req)
	if err != nil {
		t.Fatalf("ExecuteCardPayout returned error: %v", err)
	}
	if out == nil {
		t.Fatal("expected output")
	}
	if payoutReq == nil {
		t.Fatal("expected payout request to be submitted")
	}
	if got, want := dialAddress, "mntx-gateway:50051"; got != want {
		t.Fatalf("dial address mismatch: got=%q want=%q", got, want)
	}
	if got, want := payoutReq.GetAmountMinor(), int64(15000); got != want {
		t.Fatalf("amount_minor mismatch: got=%d want=%d", got, want)
	}
	if got, want := payoutReq.GetCurrency(), "RUB"; got != want {
		t.Fatalf("currency mismatch: got=%q want=%q", got, want)
	}
	if got, want := payoutReq.GetPayoutId(), ""; got != want {
		t.Fatalf("payout_id mismatch: got=%q want=%q", got, want)
	}
	if got, want := payoutReq.GetCardPan(), "2200700142860162"; got != want {
		t.Fatalf("card pan mismatch: got=%q want=%q", got, want)
	}
	if got, want := payoutReq.GetMetadata()[batchmeta.MetaPayoutTargetRef], "recipient-2"; got != want {
		t.Fatalf("target_ref metadata mismatch: got=%q want=%q", got, want)
	}
	if got, want := payoutReq.GetComment(), "batch-note-2"; got != want {
		t.Fatalf("comment mismatch: got=%q want=%q", got, want)
	}
	if _, ok := payoutReq.GetMetadata()["comment"]; ok {
		t.Fatalf("comment must not be sent in metadata")
	}
	if got, want := payoutReq.GetParentPaymentRef(), "payment-2"; got != want {
		t.Fatalf("parent_payment_ref mismatch: got=%q want=%q", got, want)
	}
}

func TestGatewayCardPayoutExecutor_ExecuteCardPayout_RequiresGatewayRegistry(t *testing.T) {
	orgID := bson.NewObjectID()

	executor := &gatewayCardPayoutExecutor{}
	_, err := executor.ExecuteCardPayout(context.Background(), sexec.StepRequest{
		Payment: &agg.Payment{
			OrganizationBoundBase: pm.OrganizationBoundBase{OrganizationRef: orgID},
			PaymentRef:            "payment-2",
			IntentSnapshot: model.PaymentIntent{
				Destination: model.PaymentEndpoint{
					Type: model.EndpointTypeCard,
					Card: &model.CardEndpoint{
						Pan:      "4111111111111111",
						ExpMonth: 3,
						ExpYear:  2030,
					},
				},
				Amount: &paymenttypes.Money{Amount: "10", Currency: "RUB"},
			},
		},
		Step: xplan.Step{
			StepRef:  "hop_4_card_payout_send",
			StepCode: "hop.4.card_payout.send",
			Action:   discovery.RailOperationSend,
			Rail:     discovery.RailCardPayout,
		},
		StepExecution: agg.StepExecution{
			StepRef:  "hop_4_card_payout_send",
			StepCode: "hop.4.card_payout.send",
			Attempt:  1,
		},
	})
	if err == nil {
		t.Fatal("expected error")
	}
	if !strings.Contains(err.Error(), "gateway registry is required") {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestGatewayCardPayoutExecutor_ExecuteCardPayout_BatchChildrenUseDistinctOperationRefsAndSharedParent(t *testing.T) {
	orgID := bson.NewObjectID()

	var payoutReqs []*mntxv1.CardPayoutRequest
	executor := &gatewayCardPayoutExecutor{
		gatewayRegistry: &fakeGatewayRegistry{
			items: []*model.GatewayInstanceDescriptor{
				{
					ID:         paymenttypes.DefaultCardsGatewayID,
					InstanceID: paymenttypes.DefaultCardsGatewayID,
					Rail:       discovery.RailCardPayout,
					InvokeURI:  "grpc://mntx-gateway:50051",
					IsEnabled:  true,
				},
			},
		},
		dialClient: func(_ context.Context, _ string) (mntxclient.Client, error) {
			return &mntxclient.Fake{
				CreateCardPayoutFn: func(_ context.Context, req *mntxv1.CardPayoutRequest) (*mntxv1.CardPayoutResponse, error) {
					payoutReqs = append(payoutReqs, req)
					return &mntxv1.CardPayoutResponse{
						Payout: &mntxv1.CardPayoutState{
							PayoutId:     req.GetOperationRef(),
							OperationRef: req.GetOperationRef(),
						},
					}, nil
				},
			}, nil
		},
	}

	payment := &agg.Payment{
		OrganizationBoundBase: pm.OrganizationBoundBase{OrganizationRef: orgID},
		PaymentRef:            "payment-3",
		IdempotencyKey:        "idem-3",
		QuotationRef:          "quote-3",
		IntentSnapshot: model.PaymentIntent{
			Ref:     "intent-3",
			Comment: "group-default-comment",
			Destination: model.PaymentEndpoint{
				Type: model.EndpointTypeCard,
				Card: &model.CardEndpoint{
					Pan:      "2200700142860161",
					ExpMonth: 3,
					ExpYear:  2030,
				},
			},
			Customer: &model.Customer{
				ID:        "cust-3",
				FirstName: "Stephan",
				LastName:  "Deshevikh",
				IP:        "198.51.100.10",
			},
			Amount: &paymenttypes.Money{
				Amount:   "1.000000",
				Currency: "USDT",
			},
		},
		QuoteSnapshot: &model.PaymentQuoteSnapshot{
			ExpectedSettlementAmount: &paymenttypes.Money{
				Amount:   "76.50",
				Currency: "RUB",
			},
			QuoteRef: "quote-3",
		},
	}

	firstReq := sexec.StepRequest{
		Payment: payment,
		Step: xplan.Step{
			StepRef:    "hop_4_card_payout_send",
			StepCode:   "hop.4.card_payout.send",
			Action:     discovery.RailOperationSend,
			Rail:       discovery.RailCardPayout,
			Gateway:    paymenttypes.DefaultCardsGatewayID,
			InstanceID: paymenttypes.DefaultCardsGatewayID,
			HopRole:    paymenttypes.QuoteRouteHopRoleDestination,
		},
		StepExecution: agg.StepExecution{
			StepRef:  "hop_4_card_payout_send",
			StepCode: "hop.4.card_payout.send",
			Attempt:  1,
		},
	}
	secondReq := sexec.StepRequest{
		Payment: payment,
		Step: xplan.Step{
			StepRef:    "hop_4_card_payout_send_2",
			StepCode:   "hop.4.card_payout.send",
			Action:     discovery.RailOperationSend,
			Rail:       discovery.RailCardPayout,
			Gateway:    paymenttypes.DefaultCardsGatewayID,
			InstanceID: paymenttypes.DefaultCardsGatewayID,
			Comment:    "recipient-2-comment",
			Metadata: map[string]string{
				batchmeta.MetaPayoutTargetRef: "recipient-2",
				batchmeta.MetaAmount:          "150",
				batchmeta.MetaCurrency:        "RUB",
				batchmeta.MetaCardPan:         "2200700142860162",
				batchmeta.MetaCardExpMonth:    "4",
				batchmeta.MetaCardExpYear:     "2030",
			},
			HopRole: paymenttypes.QuoteRouteHopRoleDestination,
		},
		StepExecution: agg.StepExecution{
			StepRef:  "hop_4_card_payout_send_2",
			StepCode: "hop.4.card_payout.send",
			Attempt:  1,
		},
	}

	if _, err := executor.ExecuteCardPayout(context.Background(), firstReq); err != nil {
		t.Fatalf("first ExecuteCardPayout returned error: %v", err)
	}
	if _, err := executor.ExecuteCardPayout(context.Background(), secondReq); err != nil {
		t.Fatalf("second ExecuteCardPayout returned error: %v", err)
	}

	if got, want := len(payoutReqs), 2; got != want {
		t.Fatalf("submitted request count mismatch: got=%d want=%d", got, want)
	}
	if got, want := payoutReqs[0].GetOperationRef(), "payment-3:hop_4_card_payout_send"; got != want {
		t.Fatalf("first operation_ref mismatch: got=%q want=%q", got, want)
	}
	if got, want := payoutReqs[1].GetOperationRef(), "payment-3:hop_4_card_payout_send_2"; got != want {
		t.Fatalf("second operation_ref mismatch: got=%q want=%q", got, want)
	}
	if payoutReqs[0].GetPayoutId() != "" || payoutReqs[1].GetPayoutId() != "" {
		t.Fatalf("expected empty payout_id for both child operations")
	}
	if got, want := payoutReqs[0].GetParentPaymentRef(), "payment-3"; got != want {
		t.Fatalf("first parent_payment_ref mismatch: got=%q want=%q", got, want)
	}
	if got, want := payoutReqs[1].GetParentPaymentRef(), "payment-3"; got != want {
		t.Fatalf("second parent_payment_ref mismatch: got=%q want=%q", got, want)
	}
	if got, want := payoutReqs[0].GetComment(), "group-default-comment"; got != want {
		t.Fatalf("first comment mismatch: got=%q want=%q", got, want)
	}
	if got, want := payoutReqs[1].GetComment(), "recipient-2-comment"; got != want {
		t.Fatalf("second comment mismatch: got=%q want=%q", got, want)
	}
	if _, ok := payoutReqs[0].GetMetadata()["comment"]; ok {
		t.Fatalf("first comment must not be sent in metadata")
	}
	if _, ok := payoutReqs[1].GetMetadata()["comment"]; ok {
		t.Fatalf("second comment must not be sent in metadata")
	}
	if payoutReqs[0].GetCardPan() == payoutReqs[1].GetCardPan() {
		t.Fatalf("expected different destination cards across child operations")
	}
}
