package shuncheng

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strconv"
	"testing"
)

func TestSignUsesDocumentedBase64LowerMD5(t *testing.T) {
	params := map[string]string{
		"service":        "get_tdcode",
		"merchant_id":    "1234567890",
		"third_order_id": "NO1",
		"amount":         "100",
		"nonce_str":      "abc",
		"req_serial_no":  "req001",
		"empty":          "",
		"sign":           "ignored",
	}

	got := SignStrings(params, "secret")
	want := "ZTM5YTQ1NDk4YmRhODJlYTYyOWQzMGI2N2M5YzFjYjQ="
	if got != want {
		t.Fatalf("SignStrings() = %s, want %s", got, want)
	}

	baseString := SignBaseString(stringMapToAny(params))
	wantBaseString := "amount=100&merchant_id=1234567890&nonce_str=abc&req_serial_no=req001&service=get_tdcode&third_order_id=NO1"
	if baseString != wantBaseString {
		t.Fatalf("SignBaseString() = %s, want %s", baseString, wantBaseString)
	}
}

func TestSignSortsNestedObjectLikeDocs(t *testing.T) {
	params := map[string]any{
		"reqSerialNo": "20260318110810747",
		"data": map[string]any{
			"merchantId":  "M1",
			"applyAmount": 10000,
			"reqId":       "R1",
			"ignored":     nil,
		},
	}

	baseString := SignBaseString(params)
	wantBaseString := `data={"applyAmount":10000,"merchantId":"M1","reqId":"R1"}&reqSerialNo=20260318110810747`
	if baseString != wantBaseString {
		t.Fatalf("SignBaseString() = %s, want %s", baseString, wantBaseString)
	}
	got := Sign(params, "secret")
	want := "N2VlOWY3OWJlZmU4YThiNjEwN2Y1NWYxNzI0ZjY0NTY="
	if got != want {
		t.Fatalf("Sign() = %s, want %s", got, want)
	}
}

func TestParsePayloadNormalizesXMLAliases(t *testing.T) {
	params, err := ParsePayload([]byte(`<schc>
<resp_code><![CDATA[200]]></resp_code>
<result_code><![CDATA[0]]></result_code>
<third_order_id><![CDATA[PAY1]]></third_order_id>
<schc_order_id><![CDATA[SC1]]></schc_order_id>
<schc_refund_id><![CDATA[RF1]]></schc_refund_id>
<refund_amount><![CDATA[100]]></refund_amount>
<status><![CDATA[11]]></status>
</schc>`))
	if err != nil {
		t.Fatalf("ParsePayload() error = %v", err)
	}
	if params["provider_order_id"] != "SC1" {
		t.Fatalf("provider_order_id = %q, want SC1", params["provider_order_id"])
	}
	if params["provider_refund_id"] != "RF1" {
		t.Fatalf("provider_refund_id = %q, want RF1", params["provider_refund_id"])
	}
}

func TestParsePayloadExtractsEscapedXMLFromHTML(t *testing.T) {
	params, err := ParsePayload([]byte(`<html><body>&lt;schc&gt;&lt;resp_code&gt;200&lt;/resp_code&gt;&lt;result_code&gt;0&lt;/result_code&gt;&lt;third_order_id&gt;PAY1&lt;/third_order_id&gt;&lt;schc_order_id&gt;SC1&lt;/schc_order_id&gt;&lt;jspay_url&gt;https://pay.example/sc1&lt;/jspay_url&gt;&lt;/schc&gt;</body></html>`))
	if err != nil {
		t.Fatalf("ParsePayload() error = %v", err)
	}
	if params["provider_order_id"] != "SC1" || params["jspay_url"] != "https://pay.example/sc1" {
		t.Fatalf("ParsePayload() = %#v, want extracted shuncheng xml", params)
	}
}

func TestParsePayloadAcceptsEscapedURLQueryInHTMLXML(t *testing.T) {
	params, err := ParsePayload([]byte(`<html><body>&lt;schc&gt;&lt;resp_code&gt;200&lt;/resp_code&gt;&lt;result_code&gt;0&lt;/result_code&gt;&lt;third_order_id&gt;PAY1&lt;/third_order_id&gt;&lt;schc_order_id&gt;SC1&lt;/schc_order_id&gt;&lt;jspay_url&gt;https://pay.example/sc1?mid=1&amp;pu=2&lt;/jspay_url&gt;&lt;/schc&gt;</body></html>`))
	if err != nil {
		t.Fatalf("ParsePayload() error = %v", err)
	}
	if params["jspay_url"] != "https://pay.example/sc1?mid=1&pu=2" {
		t.Fatalf("jspay_url = %q, want query decoded", params["jspay_url"])
	}
}

func TestParsePayloadAcceptsJSONStringXML(t *testing.T) {
	xmlText := `<?xml version="1.0" encoding="utf-16"?><schc xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><resp_code>200</resp_code><merchant_id>5169118450</merchant_id><sign>00EFB11258D694634F727A8FCCF92290</sign><cost_time>4</cost_time><sign_type>MD5</sign_type><third_order_id>PAY1</third_order_id><jspay_url>https://pay.example/sc1?mid=1&amp;pu=2</jspay_url><result_code>0</result_code><schc_order_id>SC1</schc_order_id></schc>`
	body, err := json.Marshal(xmlText)
	if err != nil {
		t.Fatalf("json marshal xml text: %v", err)
	}
	params, err := ParsePayload(body)
	if err != nil {
		t.Fatalf("ParsePayload() error = %v", err)
	}
	if params["provider_order_id"] != "SC1" || params["jspay_url"] != "https://pay.example/sc1?mid=1&pu=2" {
		t.Fatalf("ParsePayload() = %#v, want xml string decoded", params)
	}
}

func TestParsePayloadTrimsBOMBeforeXML(t *testing.T) {
	params, err := ParsePayload([]byte("\xef\xbb\xbf<schc><resp_code>200</resp_code><result_code>0</result_code><third_order_id>PAY1</third_order_id><schc_order_id>SC1</schc_order_id></schc>"))
	if err != nil {
		t.Fatalf("ParsePayload() error = %v", err)
	}
	if params["provider_order_id"] != "SC1" {
		t.Fatalf("provider_order_id = %q, want SC1", params["provider_order_id"])
	}
}

func TestParsePayloadRejectsPlainHTML(t *testing.T) {
	if _, err := ParsePayload([]byte(`<html><body>invalid semicolon; separator</body></html>`)); err == nil {
		t.Fatal("ParsePayload() error = nil, want unsupported html error")
	}
}

func TestVerifyNotifyAcceptsDocumentedAndLegacySignatures(t *testing.T) {
	client := NewClient(Config{NotifyKey: "notify-secret"})
	params := map[string]string{
		"merchant_id":    "1234567890",
		"third_order_id": "NO1",
		"schc_order_id":  "SC1",
		"amount":         "100",
		"status":         "2",
		"sign_type":      "MD5",
	}
	params["sign"] = SignStrings(params, "notify-secret")
	if result := client.VerifyNotifyDetail(params); !result.OK {
		t.Fatalf("VerifyNotifyDetail().OK = false, got=%s expected=%v", result.Got, result.Expected)
	}

	params["sign"] = SignStringsUpperMD5(params, "notify-secret")
	if result := client.VerifyNotifyDetail(params); !result.OK {
		t.Fatalf("VerifyNotifyDetail() legacy upper md5 OK = false, got=%s expected=%v", result.Got, result.Expected)
	}

	params["amount"] = "101"
	if client.VerifyNotify(params) {
		t.Fatal("VerifyNotify() = true after amount changed, want false")
	}
}

func TestCreatePaymentFetchesTokenOnceAndPostsSignedJSON(t *testing.T) {
	resetTokenCacheForTest()
	var tokenCount int
	var createCount int
	var captured map[string]string
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case endpointToken:
			tokenCount++
			w.Header().Set("Content-Type", "application/json")
			_, _ = w.Write([]byte(`{"respCode":"200","data":"token-1"}`))
		case endpointSimplePay:
			createCount++
			if r.Header.Get("Authorization") != "Bearer token-1" {
				t.Fatalf("Authorization = %q, want Bearer token-1", r.Header.Get("Authorization"))
			}
			if err := json.NewDecoder(r.Body).Decode(&captured); err != nil {
				t.Fatalf("decode create payment request: %v", err)
			}
			if captured["sign"] != SignStrings(captured, "secret-key") {
				t.Fatalf("sign = %q, want recalculated %q", captured["sign"], SignStrings(captured, "secret-key"))
			}
			w.Header().Set("Content-Type", "application/xml")
			_, _ = w.Write([]byte(`<schc><resp_code>200</resp_code><result_code>0</result_code><third_order_id>PAY1</third_order_id><schc_order_id>SC1</schc_order_id><jspay_url>https://pay.example/sc1</jspay_url></schc>`))
		default:
			t.Fatalf("unexpected path %s", r.URL.Path)
		}
	}))
	defer server.Close()

	client := NewClient(Config{
		GatewayURL: server.URL,
		MerchantID: "merchant-1",
		SecretID:   "secret-id",
		SecretKey:  "secret-key",
		NotifyURL:  "https://example.com/notify",
	})
	for i := 0; i < 2; i++ {
		resp, err := client.CreatePayment(context.Background(), CreatePaymentRequest{
			ThirdOrderID: "PAY1",
			AmountCent:   100,
			Body:         "租号订单",
		})
		if err != nil {
			t.Fatalf("CreatePayment() error = %v", err)
		}
		if !resp.OK || resp.ProviderOrderID != "SC1" || resp.JSPayURL == "" {
			t.Fatalf("CreatePayment() response = %+v", resp)
		}
	}
	if tokenCount != 1 {
		t.Fatalf("tokenCount = %d, want 1", tokenCount)
	}
	if createCount != 2 {
		t.Fatalf("createCount = %d, want 2", createCount)
	}
	if captured["notify_url"] != "https://example.com/notify" {
		t.Fatalf("notify_url = %q, want configured notify url", captured["notify_url"])
	}
}

func TestUnauthorizedRefreshesTokenOnce(t *testing.T) {
	resetTokenCacheForTest()
	var tokenCount int
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case endpointToken:
			tokenCount++
			w.Header().Set("Content-Type", "application/json")
			_, _ = w.Write([]byte(`{"respCode":"200","data":"token-` + strconv.Itoa(tokenCount) + `"}`))
		case endpointPaymentQuery:
			if r.Header.Get("Authorization") == "Bearer token-1" {
				w.WriteHeader(http.StatusUnauthorized)
				return
			}
			if r.Header.Get("Authorization") != "Bearer token-2" {
				t.Fatalf("Authorization = %q, want Bearer token-2", r.Header.Get("Authorization"))
			}
			w.Header().Set("Content-Type", "application/xml")
			_, _ = w.Write([]byte(`<schc><resp_code>200</resp_code><result_code>0</result_code><third_order_id>PAY1</third_order_id><schc_order_id>SC1</schc_order_id><status>2</status></schc>`))
		default:
			t.Fatalf("unexpected path %s", r.URL.Path)
		}
	}))
	defer server.Close()

	client := NewClient(Config{
		GatewayURL: server.URL,
		MerchantID: "merchant-1",
		SecretID:   "secret-id",
		SecretKey:  "secret-key",
	})
	resp, err := client.QueryPayment(context.Background(), "PAY1", "")
	if err != nil {
		t.Fatalf("QueryPayment() error = %v", err)
	}
	if !resp.OK || resp.Status != "paid" || resp.ProviderOrderID != "SC1" {
		t.Fatalf("QueryPayment() response = %+v", resp)
	}
	if tokenCount != 2 {
		t.Fatalf("tokenCount = %d, want 2", tokenCount)
	}
}

func resetTokenCacheForTest() {
	tokenCache.Lock()
	tokenCache.items = map[string]tokenEntry{}
	tokenCache.Unlock()
}
