package payment

import (
	"context"
	"crypto"
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha256"
	"crypto/tls"
	"crypto/x509"
	"encoding/base64"
	"encoding/json"
	"encoding/pem"
	"fmt"
	bizpayment "kra/internal/biz/payment"
	"net"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/go-pay/gopay"
	"github.com/go-pay/gopay/allinpay"
	"github.com/go-pay/gopay/douyin"
	"github.com/go-pay/gopay/lakala"
)

func paymentIdentityRSAKeyPair(t *testing.T) (*rsa.PrivateKey, string, string) {
	t.Helper()
	key, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		t.Fatal(err)
	}
	privatePEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
	publicDER, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
	if err != nil {
		t.Fatal(err)
	}
	publicPEM := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: publicDER})
	return key, string(privatePEM), string(publicPEM)
}

// GoPay clients use fixed provider hosts. Redirect their HTTPS transport to a
// local server so adapter tests still exercise the SDK request and response path.
func redirectPaymentHTTPS(t *testing.T, handler http.Handler) {
	t.Helper()
	server := httptest.NewTLSServer(handler)
	oldDefault := http.DefaultTransport
	base, ok := oldDefault.(*http.Transport)
	if !ok {
		server.Close()
		t.Fatal("http.DefaultTransport is not *http.Transport")
	}
	transport := base.Clone()
	transport.Proxy = nil
	transport.DisableKeepAlives = true
	transport.ForceAttemptHTTP2 = false
	transport.TLSClientConfig = &tls.Config{MinVersion: tls.VersionTLS12, InsecureSkipVerify: true} //nolint:gosec // local test server
	target := server.Listener.Addr().String()
	transport.DialContext = func(ctx context.Context, network, _ string) (net.Conn, error) {
		return (&net.Dialer{}).DialContext(ctx, network, target)
	}
	http.DefaultTransport = transport
	t.Cleanup(func() {
		http.DefaultTransport = oldDefault
		transport.CloseIdleConnections()
		server.Close()
	})
}

func TestLakalaKeepsQueryAndFinalTradeIdentitiesSeparate(t *testing.T) {
	created, err := lakalaCreateResult("MERCHANT-LAKALA-1", &lakala.PaymentRsp{
		ErrorCode:      lakala.ErrorCode{ReturnCode: "SUCCESS", ResultCode: "SUCCESS"},
		OrderId:        "  LAKALA-ORDER-1  ",
		PartnerOrderId: "MERCHANT-LAKALA-1",
		CodeUrl:        "https://pay.example/qr",
		SdkParams:      `{"prepay":"client-only"}`,
	})
	if err != nil {
		t.Fatal(err)
	}
	if created.QueryID != "LAKALA-ORDER-1" || created.ProviderTradeNo != "" {
		t.Fatalf("create identities = query %q, provider trade %q", created.QueryID, created.ProviderTradeNo)
	}

	redirectPaymentHTTPS(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.Method {
		case http.MethodGet:
			if r.URL.Path != "/api/v1.0/gateway/partners/PART/orders/LAKALA-ORDER-1" {
				t.Errorf("query path = %q", r.URL.Path)
			}
			_, _ = fmt.Fprint(w, `{"return_code":"SUCCESS","result_code":"PAY_SUCCESS","order_id":"LAKALA-ORDER-1","partner_order_id":"MERCHANT-LAKALA-1","channel_order_id":"CHANNEL-LAKALA-1","total_fee":100,"real_fee":100,"currency":"JPY"}`)
		case http.MethodPut:
			if r.URL.Path != "/api/v1.0/gateway/partners/PART/orders/LAKALA-ORDER-1/refunds/REFUND-LAKALA-1" {
				t.Errorf("refund path = %q", r.URL.Path)
			}
			_, _ = fmt.Fprint(w, `{"return_code":"SUCCESS","result_code":"SUCCESS","refund_id":"LAKALA-REFUND-1","partner_refund_id":"REFUND-LAKALA-1","amount":40,"currency":"JPY"}`)
		default:
			t.Errorf("unexpected method %s", r.Method)
			http.Error(w, "unexpected method", http.StatusMethodNotAllowed)
		}
	}))

	config := map[string]any{
		"partner_code": "PART", "credential_code": "credential",
		"order_id": "STATIC-CONFIG-MUST-NOT-BE-USED", "currency": "JPY",
	}
	result, err := (&lakalaAdapter{}).Query(context.Background(), created.QueryID, config)
	if err != nil {
		t.Fatal(err)
	}
	if result.TradeNo != "MERCHANT-LAKALA-1" || result.QueryID != "LAKALA-ORDER-1" || result.ProviderTradeNo != "CHANNEL-LAKALA-1" {
		t.Fatalf("query identities = %+v", result)
	}
	if result.Status != "success" || result.Amount != 100 || result.Currency != "JPY" {
		t.Fatalf("query result = %+v", result)
	}

	refund, err := (&lakalaAdapter{}).Refund(context.Background(), &bizpayment.PaymentRefundRequest{
		TradeNo: "MERCHANT-LAKALA-1", QueryID: created.QueryID,
		RefundNo: "REFUND-LAKALA-1", Amount: 40, Currency: "JPY",
	}, config)
	if err != nil {
		t.Fatal(err)
	}
	if refund.TradeNo != "MERCHANT-LAKALA-1" || refund.ProviderTradeNo != "LAKALA-REFUND-1" {
		t.Fatalf("refund result = %+v", refund)
	}
}

func TestLakalaCreateRejectsMissingOrderID(t *testing.T) {
	_, err := lakalaCreateResult("MERCHANT-LAKALA-MISSING", &lakala.PaymentRsp{
		ErrorCode:      lakala.ErrorCode{ReturnCode: "SUCCESS", ResultCode: "SUCCESS"},
		PartnerOrderId: "MERCHANT-LAKALA-MISSING",
		CodeUrl:        "https://pay.example/qr",
	})
	if err == nil || !strings.Contains(strings.ToLower(err.Error()), "order_id") {
		t.Fatalf("missing order_id error = %v", err)
	}
}

func TestLakalaQueryRejectsMissingMerchantOrderID(t *testing.T) {
	redirectPaymentHTTPS(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodGet || r.URL.Path != "/api/v1.0/gateway/partners/PART/orders/LAKALA-ORDER-MISSING-MERCHANT" {
			t.Errorf("request = %s %s", r.Method, r.URL.Path)
		}
		_, _ = fmt.Fprint(w, `{"return_code":"SUCCESS","result_code":"PAY_SUCCESS","order_id":"LAKALA-ORDER-MISSING-MERCHANT","channel_order_id":"CHANNEL-LAKALA-1","total_fee":100,"currency":"JPY"}`)
	}))

	_, err := (&lakalaAdapter{}).Query(context.Background(), "LAKALA-ORDER-MISSING-MERCHANT", map[string]any{
		"partner_code": "PART", "credential_code": "credential",
	})
	if err == nil || !strings.Contains(err.Error(), "商户订单号") {
		t.Fatalf("missing merchant order id error = %v", err)
	}
}

func TestLakalaCreateRoutesSupportedMethods(t *testing.T) {
	type testCase struct {
		name         string
		method       string
		configMethod string
		tradeNo      string
		path         string
		appID        string
	}
	cases := []testCase{
		{name: "default jsapi", tradeNo: "L-JSAPI-DEFAULT", path: "/api/v1.0/jsapi_gateway/partners/PART/orders/L-JSAPI-DEFAULT"},
		{name: "jsapi", method: "jsapi", tradeNo: "L-JSAPI", path: "/api/v1.0/jsapi_gateway/partners/PART/orders/L-JSAPI"},
		{name: "h5", method: "h5", tradeNo: "L-H5", path: "/api/v1.0/h5_payment/partners/PART/orders/L-H5"},
		{name: "mini", method: "mini", tradeNo: "L-MINI", path: "/api/v1.0/gateway/partners/PART/microapp_orders/L-MINI"},
		{name: "native", method: "native", tradeNo: "L-NATIVE", path: "/api/v1.0/gateway/partners/PART/native_orders/L-NATIVE"},
		{name: "qrcode", method: "qrcode", tradeNo: "L-QRCODE", path: "/api/v1.0/gateway/partners/PART/orders/L-QRCODE"},
		{name: "native jsapi", method: "native_jsapi", tradeNo: "L-NATIVE-JSAPI", path: "/api/v1.0/gateway/partners/PART/native_jsapi/L-NATIVE-JSAPI", appID: "wx-test-app"},
		{name: "sdk", method: "sdk", tradeNo: "L-SDK", path: "/api/v1.0/gateway/partners/PART/app_orders/L-SDK", appID: "wx-test-app"},
		{name: "web", method: "web", tradeNo: "L-WEB", path: "/api/v1.0/web_gateway/partners/PART/orders/L-WEB"},
		{name: "config method", configMethod: "h5", tradeNo: "L-CONFIG-H5", path: "/api/v1.0/h5_payment/partners/PART/orders/L-CONFIG-H5"},
	}
	expected := make(map[string]string, len(cases))
	for _, tc := range cases {
		expected[tc.path] = tc.tradeNo
	}
	redirectPaymentHTTPS(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPut {
			t.Errorf("create method = %s, want PUT", r.Method)
		}
		tradeNo, ok := expected[r.URL.Path]
		if !ok {
			t.Errorf("unexpected Lakala create path = %q", r.URL.Path)
			http.Error(w, "unexpected path", http.StatusNotFound)
			return
		}
		var body map[string]any
		if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
			t.Errorf("decode create body: %v", err)
		}
		if body["description"] != "subject" || fmt.Sprint(body["price"]) != "100" || body["channel"] != "Wechat" {
			t.Errorf("create body = %#v", body)
		}
		_, _ = fmt.Fprintf(w, `{"return_code":"SUCCESS","result_code":"SUCCESS","order_id":"ORDER-%s","partner_order_id":"%s","code_url":"https://pay.example/%s","pay_url":"https://pay.example/pay/%s","sdk_params":"{\"prepay\":\"%s\"}"}`, tradeNo, tradeNo, tradeNo, tradeNo, tradeNo)
	}))

	adapter := &lakalaAdapter{}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			extra := map[string]any{}
			if tc.method != "" {
				extra["method"] = tc.method
			}
			if tc.appID != "" {
				extra["appid"] = tc.appID
			}
			config := map[string]any{
				"partner_code": "PART", "credential_code": "credential", "channel": "Wechat", "currency": "JPY",
			}
			if tc.configMethod != "" {
				config["method"] = tc.configMethod
			}
			result, err := adapter.Create(context.Background(), &bizpayment.PaymentRequest{
				Provider: bizpayment.PaymentLakala, TradeNo: tc.tradeNo, Subject: "subject", Amount: 100,
				Currency: "JPY", Extra: extra,
			}, config)
			if err != nil {
				t.Fatal(err)
			}
			if result.QueryID != "ORDER-"+tc.tradeNo || result.ProviderTradeNo != "" {
				t.Fatalf("identities = %+v", result)
			}
			var payload lakala.PaymentRsp
			if err := json.Unmarshal(result.Payload, &payload); err != nil {
				t.Fatalf("decode payload: %v", err)
			}
			if payload.OrderId != "ORDER-"+tc.tradeNo || payload.PartnerOrderId != tc.tradeNo {
				t.Fatalf("payload = %+v", payload)
			}
		})
	}
}

func TestLakalaCreateRejectsUnknownMethod(t *testing.T) {
	_, err := (&lakalaAdapter{}).Create(context.Background(), &bizpayment.PaymentRequest{
		Provider: bizpayment.PaymentLakala, TradeNo: "L-UNKNOWN", Subject: "subject", Amount: 100,
		Currency: "JPY", Extra: map[string]any{"method": "unsupported"},
	}, map[string]any{"partner_code": "PART", "credential_code": "credential", "channel": "Wechat"})
	if err == nil || !strings.Contains(err.Error(), "不支持的下单方式") {
		t.Fatalf("unknown method error = %v", err)
	}
}

func TestAllinPayPersistsQueryIDOnlyForTransactionLookupMode(t *testing.T) {
	for _, tc := range []struct {
		name   string
		config map[string]any
		want   string
	}{
		{name: "default reqsn", config: map[string]any{}},
		{name: "explicit reqsn", config: map[string]any{"query_order_type": allinpay.OrderTypeReqSN}},
		{name: "trxid", config: map[string]any{"query_order_type": strings.ToUpper(allinpay.OrderTypeTrxId)}, want: "TRX-1"},
	} {
		t.Run(tc.name, func(t *testing.T) {
			orderType, err := allinpayOrderType(tc.config)
			if err != nil {
				t.Fatal(err)
			}
			got, err := allinpayCreateQueryID(orderType, "  TRX-1  ")
			if err != nil {
				t.Fatal(err)
			}
			if got != tc.want {
				t.Fatalf("query id = %q, want %q", got, tc.want)
			}
		})
	}
}

func TestAllinPayQueryIdentityMatchesConfiguredLookupMode(t *testing.T) {
	key, _, _ := paymentIdentityRSAKeyPair(t)
	privateKey := base64.StdEncoding.EncodeToString(x509.MarshalPKCS1PrivateKey(key))
	publicDER, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
	if err != nil {
		t.Fatal(err)
	}
	publicKey := base64.StdEncoding.EncodeToString(publicDER)
	for _, tc := range []struct {
		name          string
		orderType     string
		queryID       string
		merchantNo    string
		wantResultKey string
	}{
		{name: "merchant order number", queryID: "MERCHANT-ALLIN-1", merchantNo: "MERCHANT-ALLIN-1"},
		{name: "provider transaction number", orderType: allinpay.OrderTypeTrxId, queryID: "TRX-ALLIN-1", merchantNo: "MERCHANT-ALLIN-1", wantResultKey: "TRX-ALLIN-1"},
	} {
		t.Run(tc.name, func(t *testing.T) {
			redirectPaymentHTTPS(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				if r.Method != http.MethodPost || r.URL.Path != "/apiweb/tranx/query" {
					t.Errorf("request = %s %s", r.Method, r.URL.Path)
				}
				if err := r.ParseForm(); err != nil {
					t.Errorf("parse form: %v", err)
				}
				if tc.orderType == allinpay.OrderTypeTrxId {
					if r.Form.Get("trxid") != tc.queryID || r.Form.Get("reqsn") != "" {
						t.Errorf("transaction query form = %v", r.Form)
					}
				} else if r.Form.Get("reqsn") != tc.queryID || r.Form.Get("trxid") != "" {
					t.Errorf("merchant query form = %v", r.Form)
				}
				response := signedAllinPayResponse(t, key, gopay.BodyMap{
					"retcode": "SUCCESS", "retmsg": "ok", "reqsn": tc.merchantNo,
					"trxid": "TRX-ALLIN-1", "trxstatus": "SUCCESS", "trxamt": "100",
				})
				w.Header().Set("Content-Type", "application/json")
				_, _ = w.Write(response)
			}))

			config := map[string]any{
				"cus_id": "customer", "app_id": "app", "private_key": privateKey,
				"public_key": publicKey, "environment": "sandbox", "currency": "CNY",
			}
			if tc.orderType != "" {
				config["query_order_type"] = tc.orderType
			}
			result, err := (&allinpayAdapter{}).Query(context.Background(), tc.queryID, config)
			if err != nil {
				t.Fatal(err)
			}
			if result.TradeNo != tc.merchantNo || result.ProviderTradeNo != "TRX-ALLIN-1" || result.QueryID != tc.wantResultKey {
				t.Fatalf("query identities = %+v", result)
			}
			if result.Status != "success" || result.Amount != 100 || result.Currency != "CNY" {
				t.Fatalf("query result = %+v", result)
			}
		})
	}
}

func TestDouyinPrepayPayloadSignaturesAreVerifiable(t *testing.T) {
	key, privatePEM, _ := paymentIdentityRSAKeyPair(t)
	client, err := douyin.NewClient("merchant-douyin", "merchant-serial", "01234567890123456789012345678901", privatePEM)
	if err != nil {
		t.Fatal(err)
	}

	for _, app := range []bool{false, true} {
		result, err := douyinPrepayResult(client, "douyin-app", "MERCHANT-DOUYIN-1", "PREPAY-DOUYIN-1", app)
		if err != nil {
			t.Fatal(err)
		}
		if result.QueryID != "" || result.ProviderTradeNo != "" {
			t.Fatalf("prepay id leaked into durable identity: %+v", result)
		}

		var content, signature string
		if app {
			var params douyin.AppPayParams
			if err = json.Unmarshal(result.Payload, &params); err != nil {
				t.Fatal(err)
			}
			if params.AppId != "douyin-app" || params.PartnerId != "merchant-douyin" || params.PrepayId != "PREPAY-DOUYIN-1" {
				t.Fatalf("app params = %+v", params)
			}
			content = params.AppId + "\n" + params.Timestamp + "\n" + params.NonceStr + "\n" + params.PrepayId + "\n"
			signature = params.Sign
		} else {
			var params douyin.JSAPIPayParams
			if err = json.Unmarshal(result.Payload, &params); err != nil {
				t.Fatal(err)
			}
			if params.AppId != "douyin-app" || params.Package != "prepay_id=PREPAY-DOUYIN-1" || params.SignType != douyin.SignTypeRSA {
				t.Fatalf("JSAPI params = %+v", params)
			}
			content = params.AppId + "\n" + params.TimeStamp + "\n" + params.NonceStr + "\n" + params.Package + "\n"
			signature = params.PaySign
		}
		signatureBytes, err := base64.StdEncoding.DecodeString(signature)
		if err != nil {
			t.Fatal(err)
		}
		digest := sha256.Sum256([]byte(content))
		if err = rsa.VerifyPKCS1v15(&key.PublicKey, crypto.SHA256, digest[:], signatureBytes); err != nil {
			t.Fatalf("app=%v signature verification: %v", app, err)
		}
	}
}
