package payment

import (
	"context"
	bizpayment "kra/internal/biz/payment"
	"testing"

	"github.com/go-pay/gopay"
	"github.com/go-pay/gopay/saobei"
)

type saobeiClientStub struct {
	miniPay    func(context.Context, gopay.BodyMap) (*saobei.MiniPayRsp, error)
	barcodePay func(context.Context, gopay.BodyMap) (*saobei.BarcodePayRsp, error)
	query      func(context.Context, gopay.BodyMap) (*saobei.QueryRsp, error)
	refund     func(context.Context, gopay.BodyMap) (*saobei.RefundRsp, error)
}

func (s *saobeiClientStub) MiniPay(ctx context.Context, bm gopay.BodyMap) (*saobei.MiniPayRsp, error) {
	return s.miniPay(ctx, bm)
}

func (s *saobeiClientStub) BarcodePay(ctx context.Context, bm gopay.BodyMap) (*saobei.BarcodePayRsp, error) {
	return s.barcodePay(ctx, bm)
}

func (s *saobeiClientStub) Query(ctx context.Context, bm gopay.BodyMap) (*saobei.QueryRsp, error) {
	return s.query(ctx, bm)
}

func (s *saobeiClientStub) Refund(ctx context.Context, bm gopay.BodyMap) (*saobei.RefundRsp, error) {
	return s.refund(ctx, bm)
}

func newSaobeiTestAdapter(client saobeiClient) *saobeiAdapter {
	return &saobeiAdapter{newClient: func(map[string]any) (saobeiClient, error) {
		return client, nil
	}}
}

func TestSaobeiOKRequiresExplicitSuccess(t *testing.T) {
	for _, tc := range []struct {
		name       string
		returnCode string
		resultCode string
		want       bool
	}{
		{name: "documented success", returnCode: "01", resultCode: "01", want: true},
		{name: "text success", returnCode: "SUCCESS", resultCode: "SUCCESS", want: true},
		{name: "missing result code", returnCode: "01", want: false},
		{name: "empty response"},
		{name: "business failure", returnCode: "01", resultCode: "02"},
		{name: "communication failure", returnCode: "02", resultCode: "01"},
	} {
		t.Run(tc.name, func(t *testing.T) {
			if got := saobeiOK(tc.returnCode, tc.resultCode); got != tc.want {
				t.Fatalf("saobeiOK(%q, %q) = %v, want %v", tc.returnCode, tc.resultCode, got, tc.want)
			}
		})
	}
}

func TestSaobeiCreateKeepsQueryAndProviderTradeIDsSeparate(t *testing.T) {
	t.Run("mini pay", func(t *testing.T) {
		adapter := newSaobeiTestAdapter(&saobeiClientStub{miniPay: func(context.Context, gopay.BodyMap) (*saobei.MiniPayRsp, error) {
			return &saobei.MiniPayRsp{RspBase: saobei.RspBase{ReturnCode: "01", ResultCode: "01"}, OutTradeNo: "SAOBEI-ORDER-1"}, nil
		}})
		result, err := adapter.Create(context.Background(), &bizpayment.PaymentRequest{
			TradeNo: "LOCAL-1",
			Amount:  100,
			Extra:   map[string]any{"sub_appid": "APP-1", "open_id": "OPEN-1"},
		}, nil)
		if err != nil {
			t.Fatalf("Create() error = %v", err)
		}
		if result.TradeNo != "LOCAL-1" || result.QueryID != "SAOBEI-ORDER-1" || result.ProviderTradeNo != "" {
			t.Fatalf("Create() result = %+v", result)
		}
	})

	t.Run("barcode pay", func(t *testing.T) {
		var request gopay.BodyMap
		adapter := newSaobeiTestAdapter(&saobeiClientStub{barcodePay: func(_ context.Context, bm gopay.BodyMap) (*saobei.BarcodePayRsp, error) {
			request = bm
			return &saobei.BarcodePayRsp{RspBase: saobei.RspBase{ReturnCode: "01", ResultCode: "01"}, OutTradeNo: "SAOBEI-ORDER-2", ChannelTradeNo: "CHANNEL-2"}, nil
		}})
		result, err := adapter.Create(context.Background(), &bizpayment.PaymentRequest{
			TradeNo: "LOCAL-2",
			Amount:  200,
			Extra:   map[string]any{"method": "barcode", "auth_code": "AUTH-2"},
		}, nil)
		if err != nil {
			t.Fatalf("Create() error = %v", err)
		}
		if result.TradeNo != "LOCAL-2" || result.QueryID != "SAOBEI-ORDER-2" || result.ProviderTradeNo != "" {
			t.Fatalf("Create() result = %+v", result)
		}
		if request.GetString("auth_no") != "AUTH-2" {
			t.Fatalf("Create() auth_no = %q", request.GetString("auth_no"))
		}
	})
}

func TestSaobeiCreateRejectsMissingPlatformOrderID(t *testing.T) {
	for _, method := range []string{"", "barcode"} {
		t.Run(method, func(t *testing.T) {
			adapter := newSaobeiTestAdapter(&saobeiClientStub{
				miniPay: func(context.Context, gopay.BodyMap) (*saobei.MiniPayRsp, error) {
					return &saobei.MiniPayRsp{RspBase: saobei.RspBase{ReturnCode: "01", ResultCode: "01"}}, nil
				},
				barcodePay: func(context.Context, gopay.BodyMap) (*saobei.BarcodePayRsp, error) {
					return &saobei.BarcodePayRsp{RspBase: saobei.RspBase{ReturnCode: "01", ResultCode: "01"}}, nil
				},
			})
			extra := map[string]any{}
			if method != "" {
				extra["method"] = method
			}
			if _, err := adapter.Create(context.Background(), &bizpayment.PaymentRequest{TradeNo: "LOCAL-MISSING", Amount: 1, Extra: extra}, nil); err == nil {
				t.Fatal("Create() accepted response without out_trade_no")
			}
		})
	}
}

func TestSaobeiCreateRejectsUnknownMethodBeforeClient(t *testing.T) {
	clientCreated := false
	adapter := &saobeiAdapter{newClient: func(map[string]any) (saobeiClient, error) {
		clientCreated = true
		return &saobeiClientStub{}, nil
	}}

	_, err := adapter.Create(context.Background(), &bizpayment.PaymentRequest{
		TradeNo: "LOCAL-UNKNOWN-METHOD", Amount: 1, Extra: map[string]any{"method": "unsupported"},
	}, nil)
	if err == nil {
		t.Fatal("Create() accepted an unknown method")
	}
	if clientCreated {
		t.Fatal("Create() initialized the GoPay client before rejecting the unknown method")
	}
}

func TestSaobeiQueryRestoresMerchantTradeNoAndKeepsQueryID(t *testing.T) {
	var queryBody gopay.BodyMap
	adapter := newSaobeiTestAdapter(&saobeiClientStub{query: func(_ context.Context, bm gopay.BodyMap) (*saobei.QueryRsp, error) {
		queryBody = bm
		return &saobei.QueryRsp{
			RspBase:        saobei.RspBase{ReturnCode: "01", ResultCode: "01"},
			PayTrace:       "LOCAL-1",
			OutTradeNo:     "SAOBEI-ORDER-1",
			ChannelTradeNo: "CHANNEL-1",
			TradeState:     "SUCCESS",
			TotalFee:       "100",
			BuyerPayFee:    "90",
		}, nil
	}})

	result, err := adapter.Query(context.Background(), "SAOBEI-ORDER-1", map[string]any{"currency": "cny"})
	if err != nil {
		t.Fatalf("Query() error = %v", err)
	}
	if queryBody.GetString("out_trade_no") != "SAOBEI-ORDER-1" {
		t.Fatalf("Query() out_trade_no = %q", queryBody.GetString("out_trade_no"))
	}
	if trace := queryBody.GetString("terminal_trace"); trace == "" || trace == "SAOBEI-ORDER-1" || len(trace) > 32 {
		t.Fatalf("Query() terminal_trace = %q, want an independent query流水号", trace)
	}
	if result.TradeNo != "LOCAL-1" || result.QueryID != "SAOBEI-ORDER-1" || result.ProviderTradeNo != "CHANNEL-1" {
		t.Fatalf("Query() result = %+v", result)
	}
	if result.Status != "success" || result.Amount != 100 || result.PayerPaidAmount != 90 || result.Currency != "CNY" {
		t.Fatalf("Query() payment fields = %+v", result)
	}
}

func TestSaobeiQueryRejectsMissingPaymentTrace(t *testing.T) {
	adapter := newSaobeiTestAdapter(&saobeiClientStub{query: func(context.Context, gopay.BodyMap) (*saobei.QueryRsp, error) {
		return &saobei.QueryRsp{RspBase: saobei.RspBase{ReturnCode: "01", ResultCode: "01"}, OutTradeNo: "SAOBEI-ORDER-1", TradeState: "NOTPAY"}, nil
	}})

	if _, err := adapter.Query(context.Background(), "SAOBEI-ORDER-1", nil); err == nil {
		t.Fatal("Query() accepted a response without pay_trace")
	}
}

func TestSaobeiQueryRejectsInvalidTotalFee(t *testing.T) {
	adapter := newSaobeiTestAdapter(&saobeiClientStub{query: func(context.Context, gopay.BodyMap) (*saobei.QueryRsp, error) {
		return &saobei.QueryRsp{
			RspBase:     saobei.RspBase{ReturnCode: "01", ResultCode: "01"},
			PayTrace:    "LOCAL-1",
			OutTradeNo:  "SAOBEI-ORDER-1",
			TradeState:  "SUCCESS",
			TotalFee:    "invalid",
			BuyerPayFee: "90",
		}, nil
	}})

	if _, err := adapter.Query(context.Background(), "SAOBEI-ORDER-1", nil); err == nil {
		t.Fatal("Query() accepted an invalid total_fee")
	}
}

func TestSaobeiQueryTreatsPayingAsPending(t *testing.T) {
	adapter := newSaobeiTestAdapter(&saobeiClientStub{query: func(context.Context, gopay.BodyMap) (*saobei.QueryRsp, error) {
		return &saobei.QueryRsp{
			RspBase:        saobei.RspBase{ReturnCode: "01", ResultCode: saobei.ResultCodePaying},
			TerminalTrace:  "LOCAL-QUERY-1",
			OutTradeNo:     "SAOBEI-ORDER-1",
			TradeState:     "SUCCESS",
			TotalFee:       "50",
			ChannelTradeNo: "CHANNEL-1",
		}, nil
	}})

	result, err := adapter.Query(context.Background(), "SAOBEI-ORDER-1", nil)
	if err != nil {
		t.Fatalf("Query() error = %v", err)
	}
	if result.Status != "pending" {
		t.Fatalf("Query() status = %q, want pending", result.Status)
	}
}

func TestSaobeiRefundPrefersPersistedQueryID(t *testing.T) {
	var refundBody gopay.BodyMap
	adapter := newSaobeiTestAdapter(&saobeiClientStub{refund: func(_ context.Context, bm gopay.BodyMap) (*saobei.RefundRsp, error) {
		refundBody = bm
		return &saobei.RefundRsp{RspBase: saobei.RspBase{ReturnCode: "01", ResultCode: "01"}, TerminalTrace: "LOCAL-REFUND-1", OutTradeNo: "SAOBEI-ORDER-1", OutRefundNo: "SAOBEI-REFUND-1", RefundFee: "50"}, nil
	}})

	result, err := adapter.Refund(context.Background(), &bizpayment.PaymentRefundRequest{
		TradeNo: "LOCAL-1", QueryID: "SAOBEI-ORDER-1", ProviderTradeNo: "CHANNEL-1",
		RefundNo: "LOCAL-REFUND-1", Amount: 50, Currency: "cny",
	}, map[string]any{"refund_extra": map[string]any{"out_refund_no": "MUST-NOT-BE-SENT"}})
	if err != nil {
		t.Fatalf("Refund() error = %v", err)
	}
	if refundBody.GetString("out_trade_no") != "SAOBEI-ORDER-1" {
		t.Fatalf("Refund() out_trade_no = %q, want persisted QueryID", refundBody.GetString("out_trade_no"))
	}
	if refundBody.GetString("terminal_trace") != "LOCAL-REFUND-1" {
		t.Fatalf("Refund() terminal_trace = %q, want refund number", refundBody.GetString("terminal_trace"))
	}
	if result.TradeNo != "LOCAL-1" || result.ProviderTradeNo != "SAOBEI-REFUND-1" || result.Amount != 50 || result.Currency != "CNY" {
		t.Fatalf("Refund() result = %+v", result)
	}
	if _, ok := refundBody["out_refund_no"]; ok {
		t.Fatal("Refund() sent unsupported out_refund_no request field")
	}
}

func TestSaobeiRefundFallsBackToMerchantTradeNo(t *testing.T) {
	var refundBody gopay.BodyMap
	adapter := newSaobeiTestAdapter(&saobeiClientStub{refund: func(_ context.Context, bm gopay.BodyMap) (*saobei.RefundRsp, error) {
		refundBody = bm
		return &saobei.RefundRsp{RspBase: saobei.RspBase{ReturnCode: "01", ResultCode: "01"}, TerminalTrace: "LOCAL-REFUND-2", OutRefundNo: "SAOBEI-REFUND-2", RefundFee: "25"}, nil
	}})

	_, err := adapter.Refund(context.Background(), &bizpayment.PaymentRefundRequest{
		TradeNo: "LOCAL-2", RefundNo: "LOCAL-REFUND-2", Amount: 25, Currency: "CNY",
	}, nil)
	if err != nil {
		t.Fatalf("Refund() error = %v", err)
	}
	if refundBody.GetString("out_trade_no") != "LOCAL-2" {
		t.Fatalf("Refund() out_trade_no = %q, want merchant trade number fallback", refundBody.GetString("out_trade_no"))
	}
	if refundBody.GetString("terminal_trace") != "LOCAL-REFUND-2" {
		t.Fatalf("Refund() terminal_trace = %q, want refund number", refundBody.GetString("terminal_trace"))
	}
}

func TestSaobeiRefundTreatsPayingAsPending(t *testing.T) {
	adapter := newSaobeiTestAdapter(&saobeiClientStub{refund: func(context.Context, gopay.BodyMap) (*saobei.RefundRsp, error) {
		return &saobei.RefundRsp{
			RspBase:       saobei.RspBase{ReturnCode: "01", ResultCode: saobei.ResultCodePaying},
			TerminalTrace: "LOCAL-REFUND-3",
			OutRefundNo:   "SAOBEI-REFUND-3",
			RefundFee:     "25",
		}, nil
	}})

	result, err := adapter.Refund(context.Background(), &bizpayment.PaymentRefundRequest{
		TradeNo: "LOCAL-3", RefundNo: "LOCAL-REFUND-3", Amount: 25, Currency: "CNY",
	}, nil)
	if err != nil {
		t.Fatalf("Refund() error = %v", err)
	}
	if result.Status != "pending" || result.ProviderTradeNo != "SAOBEI-REFUND-3" {
		t.Fatalf("Refund() result = %+v", result)
	}
}

func TestSaobeiRefundRejectsMismatchedAmount(t *testing.T) {
	adapter := newSaobeiTestAdapter(&saobeiClientStub{refund: func(context.Context, gopay.BodyMap) (*saobei.RefundRsp, error) {
		return &saobei.RefundRsp{
			RspBase:       saobei.RspBase{ReturnCode: "01", ResultCode: "01"},
			TerminalTrace: "LOCAL-REFUND-1",
			OutRefundNo:   "SAOBEI-REFUND-1",
			RefundFee:     "51",
		}, nil
	}})

	if _, err := adapter.Refund(context.Background(), &bizpayment.PaymentRefundRequest{
		TradeNo: "LOCAL-1", RefundNo: "LOCAL-REFUND-1", Amount: 50, Currency: "CNY",
	}, nil); err == nil {
		t.Fatal("Refund() accepted a mismatched refund amount")
	}
}
