package binance

import (
	"context"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strconv"
	"strings"
	"testing"
	"time"

	"hyapp/pkg/xerr"
	"hyapp/services/wallet-service/internal/config"
	"hyapp/services/wallet-service/internal/service/wallet/ports"
)

func TestFindUSDTTransferMatchesDepositAndSignedRequest(t *testing.T) {
	var sawDepositRequest bool
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/api/v3/time":
			_, _ = w.Write([]byte(`{"serverTime":1783345000000}`))
		case "/sapi/v1/capital/deposit/hisrec":
			sawDepositRequest = true
			if r.Header.Get("X-MBX-APIKEY") != "api-lalu" {
				t.Fatalf("X-MBX-APIKEY mismatch")
			}
			values := r.URL.Query()
			signature := values.Get("signature")
			values.Del("signature")
			if signature != signQueryString(values.Encode(), "secret-lalu") {
				t.Fatalf("signature mismatch: query=%s signature=%s", values.Encode(), signature)
			}
			if values.Get("timestamp") != "1783345000000" || values.Get("recvWindow") != "10000" || values.Get("coin") != "USDT" || values.Get("limit") != "1000" {
				t.Fatalf("signed deposit query mismatch: %s", values.Encode())
			}
			if values.Get("endTime") != "1783345000000" || values.Get("startTime") != "1775569000000" {
				t.Fatalf("deposit bounded window mismatch: %s", values.Encode())
			}
			_, _ = w.Write([]byte(`[{
				"id":"deposit-1",
				"amount":"500.00000000",
				"coin":"USDT",
				"network":"TRX",
				"status":1,
				"address":"TNwqCkF8cnM4XiwjnbbKTfdZvFDqVoa1TN",
				"txId":"Off-chain transfer 388611987194",
				"transferType":1,
				"insertTime":1783344000000,
				"completeTime":1783344025000,
				"confirmTimes":"1/1",
				"unlockConfirm":0,
				"walletType":0
			}]`))
		default:
			http.NotFound(w, r)
		}
	}))
	defer server.Close()

	client := New(config.BinanceConfig{
		Enabled:     true,
		APIBaseURL:  server.URL,
		RecvWindow:  10000,
		HTTPTimeout: time.Second,
		Accounts: map[string]config.BinanceAccountConfig{
			"lalu": {APIKey: "api-lalu", APISecretKey: "secret-lalu"},
		},
	})
	record, err := client.FindUSDTTransfer(context.Background(), ports.BinanceTransferLookupRequest{
		AppCode:     "lalu",
		ExternalID:  "388611987194",
		Coin:        "USDT",
		Network:     "TRX",
		ToAddress:   "TNwqCkF8cnM4XiwjnbbKTfdZvFDqVoa1TN",
		AmountMinor: 50000,
	})
	if err != nil {
		t.Fatalf("FindUSDTTransfer failed: %v", err)
	}
	if !sawDepositRequest {
		t.Fatalf("deposit history endpoint was not called")
	}
	if record.Source != "deposit_history" || record.TxID != "Off-chain transfer 388611987194" || record.AmountMinor != 50000 || record.Network != "TRX" || !strings.Contains(record.RawJSON, `"txId"`) {
		t.Fatalf("deposit record mismatch: %+v", record)
	}
}

func TestFindUSDTTransferOffchainUsesDepositHistoryOnly(t *testing.T) {
	const address = "TLv7wERsM42ZeobMrrKVC5C72d3gFUa44J"
	var depositCalls int
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/api/v3/time":
			_, _ = w.Write([]byte(`{"serverTime":1784034000000}`))
		case "/sapi/v1/capital/deposit/hisrec":
			depositCalls++
			_, _ = w.Write([]byte(`[{
				"id":"deposit-aslan-offchain",
				"amount":"300.14000000",
				"coin":"USDT",
				"network":"TRX",
				"status":1,
				"address":"` + address + `",
				"txId":"Off-chain transfer 391252658053",
				"insertTime":1784033439000
			}]`))
		case "/sapi/v1/pay/transactions":
			t.Fatalf("explicit Off-chain verification must not query Pay History")
		default:
			http.NotFound(w, r)
		}
	}))
	defer server.Close()

	client := New(config.BinanceConfig{
		Enabled:     true,
		APIBaseURL:  server.URL,
		HTTPTimeout: time.Second,
		Accounts:    map[string]config.BinanceAccountConfig{"aslan": {APIKey: "api-aslan", APISecretKey: "secret-aslan"}},
	})
	record, err := client.FindUSDTTransfer(context.Background(), ports.BinanceTransferLookupRequest{
		AppCode:     "aslan",
		ExternalID:  "391252658053",
		Coin:        "USDT",
		Network:     "OFFCHAIN",
		ToAddress:   address,
		AmountMinor: 30014,
	})
	if err != nil {
		t.Fatalf("explicit Off-chain lookup failed: %v", err)
	}
	if depositCalls != 1 || record.Source != "deposit_history" || record.TxID != "Off-chain transfer 391252658053" || record.Network != "TRX" || record.AmountMinor != 30014 || record.Address != address {
		t.Fatalf("Off-chain deposit mismatch: calls=%d record=%+v", depositCalls, record)
	}
}

func TestFindUSDTTransferRejectsDepositMismatches(t *testing.T) {
	baseReq := ports.BinanceTransferLookupRequest{
		AppCode:     "lalu",
		ExternalID:  "388611987194",
		Coin:        "USDT",
		Network:     "TRX",
		ToAddress:   "TNwqCkF8cnM4XiwjnbbKTfdZvFDqVoa1TN",
		AmountMinor: 50000,
	}
	baseRecord := depositRecord{
		Amount:  "500",
		Coin:    "USDT",
		Network: "TRX",
		Status:  1,
		Address: "TNwqCkF8cnM4XiwjnbbKTfdZvFDqVoa1TN",
		TxID:    "Off-chain transfer 388611987194",
	}
	cases := []struct {
		name    string
		mutate  func(*depositRecord)
		message string
	}{
		{name: "status", mutate: func(r *depositRecord) { r.Status = 0 }, message: "status is not successful"},
		{name: "network", mutate: func(r *depositRecord) { r.Network = "BSC" }, message: "network mismatch"},
		{name: "address", mutate: func(r *depositRecord) { r.Address = "TDifferentAddress" }, message: "address mismatch"},
		{name: "amount", mutate: func(r *depositRecord) { r.Amount = "499.99" }, message: "amount mismatch"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			record := baseRecord
			tc.mutate(&record)
			_, err := matchDepositRecord(baseReq, []depositRecord{record})
			if !xerr.IsCode(err, xerr.Conflict) || !strings.Contains(err.Error(), tc.message) {
				t.Fatalf("expected conflict %q, got %v", tc.message, err)
			}
		})
	}
}

func TestFindUSDTTransferFallsBackToIncomingPayC2C(t *testing.T) {
	const (
		serverTimeMS = int64(1783345000000)
		orderTimeMS  = int64(1779713682799)
		orderID      = "433527303245463552"
	)
	var sawPayRequest bool
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/api/v3/time":
			_, _ = w.Write([]byte(`{"serverTime":1783345000000}`))
		case "/sapi/v1/capital/deposit/hisrec":
			_, _ = w.Write([]byte(`[]`))
		case "/sapi/v1/pay/transactions":
			sawPayRequest = true
			if r.Header.Get("X-MBX-APIKEY") != "api-yumi" {
				t.Fatalf("Pay API key mismatch")
			}
			values := r.URL.Query()
			signature := values.Get("signature")
			values.Del("signature")
			if signature != signQueryString(values.Encode(), "secret-yumi") {
				t.Fatalf("Pay signature mismatch: query=%s signature=%s", values.Encode(), signature)
			}
			if values.Get("startTime") != "1779710082799" || values.Get("endTime") != "1779717282799" || values.Get("limit") != "100" || values.Get("timestamp") != strconv.FormatInt(serverTimeMS, 10) {
				t.Fatalf("Pay bounded query mismatch: %s", values.Encode())
			}
			_, _ = w.Write([]byte(`{
				"code":"000000",
				"message":"success",
				"success":true,
				"data":[{
					"orderType":"C2C",
					"orderId":433527303245463552,
					"transactionId":"P_A223BU5M4Y171116",
					"transactionTime":1783344000000,
					"amount":"100.00000000",
					"currency":"USDT",
					"walletType":1,
					"payerInfo":{"name":"must-not-persist","binanceId":"payer-id"},
					"receiverInfo":{"name":"must-not-persist","binanceId":"receiver-id"}
				}]
			}`))
		default:
			http.NotFound(w, r)
		}
	}))
	defer server.Close()

	client := New(config.BinanceConfig{
		Enabled:     true,
		APIBaseURL:  server.URL,
		RecvWindow:  10000,
		HTTPTimeout: time.Second,
		Accounts: map[string]config.BinanceAccountConfig{
			"yumi": {APIKey: "api-yumi", APISecretKey: "secret-yumi"},
		},
	})
	record, err := client.FindUSDTTransfer(context.Background(), ports.BinanceTransferLookupRequest{
		AppCode:     "yumi",
		ExternalID:  orderID,
		Coin:        "USDT",
		Network:     "TRX",
		ToAddress:   "TConfiguredChainAddress",
		AmountMinor: 10000,
		OrderTimeMS: orderTimeMS,
	})
	if err != nil {
		t.Fatalf("FindUSDTTransfer Pay fallback failed: %v", err)
	}
	if !sawPayRequest {
		t.Fatalf("Pay history endpoint was not called after deposit miss")
	}
	if record.Source != "pay_history" || record.ID != orderID || record.TxID != "P_A223BU5M4Y171116" || record.AmountMinor != 10000 || record.Address != "" {
		t.Fatalf("Pay transfer record mismatch: %+v", record)
	}
	if strings.Contains(record.RawJSON, "payerInfo") || strings.Contains(record.RawJSON, "receiverInfo") || !strings.Contains(record.RawJSON, `"orderId":"`+orderID+`"`) {
		t.Fatalf("Pay audit snapshot must retain evidence without account PII: %s", record.RawJSON)
	}
}

func TestFindUSDTTransferC2CUsesPayAndAcceptsUSDC(t *testing.T) {
	const orderID = "440537037787594752"
	var sawC2C bool
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/api/v3/time":
			_, _ = w.Write([]byte(`{"serverTime":1784018000000}`))
		case "/sapi/v1/pay/transactions":
			sawC2C = true
			values := r.URL.Query()
			signature := values.Get("signature")
			values.Del("signature")
			if signature != signQueryString(values.Encode(), "secret-lalu") {
				t.Fatalf("C2C signature mismatch: %s", values.Encode())
			}
			if values.Get("limit") != "100" {
				t.Fatalf("C2C query mismatch: %s", values.Encode())
			}
			_, _ = w.Write([]byte(`{"code":"000000","success":true,"data":[{"orderType":"C2C","orderId":"440537037787594752","transactionId":"P_A22V8RE4HQS71114","transactionTime":1782977844437,"amount":"200","currency":"USDC","walletType":1}]}`))
		default:
			t.Fatalf("C2C lookup must not call another history endpoint: %s", r.URL.Path)
		}
	}))
	defer server.Close()
	client := New(config.BinanceConfig{Enabled: true, APIBaseURL: server.URL, RecvWindow: 10000, HTTPTimeout: time.Second, Accounts: map[string]config.BinanceAccountConfig{"lalu": {APIKey: "api-lalu", APISecretKey: "secret-lalu"}}})
	record, err := client.FindUSDTTransfer(context.Background(), ports.BinanceTransferLookupRequest{AppCode: "lalu", ExternalID: orderID, Coin: "USDT", Network: "C2C", AmountMinor: 20000, OrderTimeMS: 1784017560000})
	if err != nil {
		t.Fatalf("C2C lookup failed: %v", err)
	}
	if !sawC2C || record.Source != "pay_history" || record.ID != orderID || record.Coin != "USDC" || record.AmountMinor != 20000 {
		t.Fatalf("C2C record mismatch: %+v", record)
	}
}

func TestFindUSDTTransferDoesNotBypassDepositConflictWithPay(t *testing.T) {
	var payCalls int
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/api/v3/time":
			_, _ = w.Write([]byte(`{"serverTime":1783345000000}`))
		case "/sapi/v1/capital/deposit/hisrec":
			_, _ = w.Write([]byte(`[{
				"id":"deposit-conflict","amount":"100","coin":"USDT","network":"TRX","status":1,
				"address":"TDifferentAddress","txId":"Off-chain transfer 433527303245463552"
			}]`))
		case "/sapi/v1/pay/transactions":
			payCalls++
			_, _ = w.Write([]byte(`{"code":"000000","data":[]}`))
		default:
			http.NotFound(w, r)
		}
	}))
	defer server.Close()

	client := New(config.BinanceConfig{
		APIBaseURL: server.URL,
		Accounts:   map[string]config.BinanceAccountConfig{"lalu": {APIKey: "api-lalu", APISecretKey: "secret-lalu"}},
	})
	_, err := client.FindUSDTTransfer(context.Background(), ports.BinanceTransferLookupRequest{
		AppCode: "lalu", ExternalID: "433527303245463552", Coin: "USDT", Network: "TRX",
		ToAddress: "TExpectedAddress", AmountMinor: 10000,
	})
	if !xerr.IsCode(err, xerr.Conflict) || !strings.Contains(err.Error(), "address mismatch") {
		t.Fatalf("deposit conflict must be preserved, got %v", err)
	}
	if payCalls != 0 {
		t.Fatalf("Pay fallback must not bypass a conflicting deposit, calls=%d", payCalls)
	}
}

func TestMatchPayRecordRejectsOutgoingAndMismatchedTransfers(t *testing.T) {
	baseReq := ports.BinanceTransferLookupRequest{
		ExternalID:  "433527303245463552",
		Coin:        "USDT",
		AmountMinor: 10000,
	}
	baseRecord := payRecord{
		OrderType: "C2C", OrderID: "433527303245463552", TransactionID: "P_A223BU5M4Y171116",
		Amount: "100", Currency: "USDT",
	}
	cases := []struct {
		name    string
		mutate  func(*payRecord)
		message string
	}{
		{name: "outgoing", mutate: func(r *payRecord) { r.Amount = "-100" }, message: "not income"},
		{name: "zero", mutate: func(r *payRecord) { r.Amount = "0" }, message: "not income"},
		{name: "order type", mutate: func(r *payRecord) { r.OrderType = "PAY" }, message: "not c2c"},
		{name: "currency", mutate: func(r *payRecord) { r.Currency = "USDC" }, message: "currency mismatch"},
		{name: "amount", mutate: func(r *payRecord) { r.Amount = "99.99" }, message: "amount mismatch"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			record := baseRecord
			tc.mutate(&record)
			_, err := matchPayRecord(baseReq, []payRecord{record})
			if !xerr.IsCode(err, xerr.Conflict) || !strings.Contains(err.Error(), tc.message) {
				t.Fatalf("expected conflict %q, got %v", tc.message, err)
			}
		})
	}
}

func TestFindUSDTTransferMissingAccountReturnsConfiguredError(t *testing.T) {
	client := New(config.BinanceConfig{
		Enabled:    true,
		APIBaseURL: "https://binance.invalid",
		Accounts: map[string]config.BinanceAccountConfig{
			"lalu": {APIKey: "api-lalu", APISecretKey: "secret-lalu"},
		},
	})
	_, err := client.FindUSDTTransfer(context.Background(), ports.BinanceTransferLookupRequest{
		AppCode:     "aslan",
		ExternalID:  "388611987194",
		Coin:        "USDT",
		Network:     "TRX",
		ToAddress:   "TNwqCkF8cnM4XiwjnbbKTfdZvFDqVoa1TN",
		AmountMinor: 50000,
	})
	if !xerr.IsCode(err, xerr.Unavailable) || !strings.Contains(err.Error(), "binance account is not configured") {
		t.Fatalf("missing account should be explicit, got %v", err)
	}
}

func TestAccountForAppSupportsAllProductionApps(t *testing.T) {
	client := New(config.BinanceConfig{
		Accounts: map[string]config.BinanceAccountConfig{
			"lalu":  {APIKey: "api-lalu", APISecretKey: "secret-lalu"},
			"yumi":  {APIKey: "api-yumi", APISecretKey: "secret-yumi"},
			"aslan": {APIKey: "api-aslan", APISecretKey: "secret-aslan"},
		},
	})
	for _, appCode := range []string{"lalu", "yumi", "aslan"} {
		account, ok := client.accountForApp(appCode)
		if !ok || account.apiKey != "api-"+appCode || account.apiSecretKey != "secret-"+appCode {
			t.Fatalf("Binance account mapping mismatch for %s: ok=%v account=%+v", appCode, ok, account)
		}
	}
}

func TestDepositTxIDMatchesOffchainVariants(t *testing.T) {
	if !depositTxIDMatches("Off-chain transfer 388611987194", "388611987194") {
		t.Fatalf("plain off-chain id should match Binance txId")
	}
	if !depositTxIDMatches("Off-chain transfer 388611987194", "Off-chain transfer 388611987194") {
		t.Fatalf("full off-chain txId should match itself")
	}
	if depositTxIDMatches("Off-chain transfer 388611987194", "388611987195") {
		t.Fatalf("different off-chain id must not match")
	}
}

func TestSignQueryStringStable(t *testing.T) {
	values := url.Values{}
	values.Set("coin", "USDT")
	values.Set("timestamp", "1783345000000")
	signature := signQueryString(values.Encode(), "secret-lalu")
	if signature != signQueryString("coin=USDT&timestamp=1783345000000", "secret-lalu") {
		t.Fatalf("signature must be based on encoded query string")
	}
}
