package route

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"os"
	"strings"
	"testing"

	"github.com/assimon/luuu/model/dao"
	"github.com/assimon/luuu/model/mdb"
	"github.com/assimon/luuu/util/log"
	"github.com/assimon/luuu/util/sign"
	"github.com/labstack/echo/v4"
	"github.com/spf13/viper"
)

const testAPIToken = "test-secret-token"

func setupTestEnv(t *testing.T) *echo.Echo {
	t.Helper()

	tmpDir := t.TempDir()

	// minimal viper config
	viper.Reset()
	viper.Set("db_type", "sqlite")
	viper.Set("api_auth_token", testAPIToken)
	viper.Set("app_uri", "http://localhost:8080")
	viper.Set("order_expiration_time", 10)
	viper.Set("api_rate_url", "")
	viper.Set("forced_usdt_rate", 7.0)
	viper.Set("runtime_root_path", tmpDir)
	viper.Set("log_save_path", tmpDir)
	viper.Set("sqlite_database_filename", tmpDir+"/test.db")
	viper.Set("runtime_sqlite_filename", tmpDir+"/runtime.db")

	log.Init()

	// init config paths
	os.Setenv("EPUSDT_CONFIG", tmpDir)
	defer os.Unsetenv("EPUSDT_CONFIG")

	// init DB
	if err := dao.DBInit(); err != nil {
		t.Fatalf("DBInit: %v", err)
	}
	if err := dao.RuntimeInit(); err != nil {
		t.Fatalf("RuntimeInit: %v", err)
	}

	// ensure tables exist (MdbTableInit uses sync.Once, so migrate directly)
	dao.Mdb.AutoMigrate(&mdb.Orders{}, &mdb.WalletAddress{}, &mdb.SupportedAsset{})

	// seed wallet addresses
	dao.Mdb.Create(&mdb.WalletAddress{Network: mdb.NetworkTron, Address: "TTestTronAddress001", Status: mdb.TokenStatusEnable})
	dao.Mdb.Create(&mdb.WalletAddress{Network: mdb.NetworkSolana, Address: "SolTestAddress001", Status: mdb.TokenStatusEnable})
	// seed supported assets if empty
	var supportCnt int64
	dao.Mdb.Model(&mdb.SupportedAsset{}).Count(&supportCnt)
	if supportCnt == 0 {
		dao.Mdb.Create(&[]mdb.SupportedAsset{
			{Network: mdb.NetworkTron, Token: "TRX", Status: mdb.TokenStatusEnable},
			{Network: mdb.NetworkTron, Token: "USDT", Status: mdb.TokenStatusEnable},
			{Network: mdb.NetworkSolana, Token: "SOL", Status: mdb.TokenStatusEnable},
			{Network: mdb.NetworkSolana, Token: "USDT", Status: mdb.TokenStatusEnable},
			{Network: mdb.NetworkSolana, Token: "USDC", Status: mdb.TokenStatusEnable},
			{Network: mdb.NetworkEthereum, Token: "USDT", Status: mdb.TokenStatusEnable},
			{Network: mdb.NetworkEthereum, Token: "USDC", Status: mdb.TokenStatusEnable},
			{Network: mdb.NetworkBsc, Token: "USDT", Status: mdb.TokenStatusEnable},
			{Network: mdb.NetworkBsc, Token: "USDC", Status: mdb.TokenStatusEnable},
			{Network: mdb.NetworkPolygon, Token: "USDT", Status: mdb.TokenStatusEnable},
			{Network: mdb.NetworkPolygon, Token: "USDC", Status: mdb.TokenStatusEnable},
			{Network: mdb.NetworkPlasma, Token: "USDT", Status: mdb.TokenStatusEnable},
		})
	}

	e := echo.New()
	RegisterRoute(e)
	return e
}

func signBody(body map[string]interface{}) map[string]interface{} {
	sig, _ := sign.Get(body, testAPIToken)
	body["signature"] = sig
	return body
}

func doPost(e *echo.Echo, path string, body map[string]interface{}) *httptest.ResponseRecorder {
	jsonBytes, _ := json.Marshal(body)
	req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(string(jsonBytes)))
	req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
	rec := httptest.NewRecorder()
	e.ServeHTTP(rec, req)
	return rec
}

// TestCreateOrderEpusdtDefaultTron tests the epusdt compatibility route defaults to tron network.
func TestCreateOrderEpusdtDefaultTron(t *testing.T) {
	e := setupTestEnv(t)

	body := signBody(map[string]interface{}{
		"order_id":   "test-tron-001",
		"amount":     1.00,
		"notify_url": "http://localhost/notify",
	})

	rec := doPost(e, "/payments/epusdt/v1/order/create-transaction", body)
	t.Logf("Status: %d, Body: %s", rec.Code, rec.Body.String())

	if rec.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d", rec.Code)
	}

	var resp map[string]interface{}
	if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}

	data, ok := resp["data"].(map[string]interface{})
	if !ok {
		t.Fatalf("expected data in response, got: %v", resp)
	}

	if data["trade_id"] == nil || data["trade_id"] == "" {
		t.Error("expected trade_id in response")
	}
	if data["receive_address"] != "TTestTronAddress001" {
		t.Errorf("expected tron address, got: %v", data["receive_address"])
	}
	t.Logf("Order created: trade_id=%v address=%v amount=%v", data["trade_id"], data["receive_address"], data["actual_amount"])
}

// TestCreateOrderGmpayV1Solana tests the gmpay route with solana network.
func TestCreateOrderGmpayV1Solana(t *testing.T) {
	e := setupTestEnv(t)

	body := signBody(map[string]interface{}{
		"order_id":   "test-sol-001",
		"amount":     1.00,
		"token":      "usdt",
		"currency":   "cny",
		"network":    "solana",
		"notify_url": "http://localhost/notify",
	})

	rec := doPost(e, "/payments/gmpay/v1/order/create-transaction", body)
	t.Logf("Status: %d, Body: %s", rec.Code, rec.Body.String())

	if rec.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d", rec.Code)
	}

	var resp map[string]interface{}
	if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}

	data, ok := resp["data"].(map[string]interface{})
	if !ok {
		t.Fatalf("expected data in response, got: %v", resp)
	}

	if data["trade_id"] == nil || data["trade_id"] == "" {
		t.Error("expected trade_id in response")
	}
	if data["receive_address"] != "SolTestAddress001" {
		t.Errorf("expected solana address, got: %v", data["receive_address"])
	}
	t.Logf("Order created: trade_id=%v address=%v amount=%v", data["trade_id"], data["receive_address"], data["actual_amount"])
}

// TestCreateOrderGmpayV1SolNative tests creating an order for native SOL token.
func TestCreateOrderGmpayV1SolNative(t *testing.T) {
	e := setupTestEnv(t)

	body := signBody(map[string]interface{}{
		"order_id":   "test-sol-native-001",
		"amount":     0.05,
		"token":      "sol",
		"currency":   "usd",
		"network":    "solana",
		"notify_url": "http://localhost/notify",
	})

	rec := doPost(e, "/payments/gmpay/v1/order/create-transaction", body)
	t.Logf("Status: %d, Body: %s", rec.Code, rec.Body.String())

	var resp map[string]interface{}
	json.Unmarshal(rec.Body.Bytes(), &resp)
	t.Logf("Response: %v", resp)

	// This may fail if rate API is not configured, which is expected in test
	// The important thing is the route accepts the request with network=solana token=sol
	if rec.Code != http.StatusOK {
		t.Logf("Note: non-200 may be expected if rate API is not configured for SOL")
	}
}

func doGet(e *echo.Echo, path string) *httptest.ResponseRecorder {
	req := httptest.NewRequest(http.MethodGet, path, nil)
	req.Header.Set("Authorization", testAPIToken)
	rec := httptest.NewRecorder()
	e.ServeHTTP(rec, req)
	return rec
}

func doPostWithToken(e *echo.Echo, path string, body map[string]interface{}) *httptest.ResponseRecorder {
	jsonBytes, _ := json.Marshal(body)
	req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(string(jsonBytes)))
	req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
	req.Header.Set("Authorization", testAPIToken)
	rec := httptest.NewRecorder()
	e.ServeHTTP(rec, req)
	return rec
}

func parseResp(t *testing.T, rec *httptest.ResponseRecorder) map[string]interface{} {
	t.Helper()
	var resp map[string]interface{}
	if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
	return resp
}

func TestGetSupportedAssetsPublic(t *testing.T) {
	e := setupTestEnv(t)

	req := httptest.NewRequest(http.MethodGet, "/payments/gmpay/v1/supported-assets", nil)
	rec := httptest.NewRecorder()
	e.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d, body=%s", rec.Code, rec.Body.String())
	}

	resp := parseResp(t, rec)
	if resp["status_code"].(float64) != 200 {
		t.Fatalf("expected status_code=200, got %v", resp["status_code"])
	}

	data, ok := resp["data"].(map[string]interface{})
	if !ok {
		t.Fatalf("expected object data, got %T", resp["data"])
	}
	supports, ok := data["supports"].([]interface{})
	if !ok {
		t.Fatalf("expected supports array, got %T", data["supports"])
	}
	if len(supports) < 6 {
		t.Fatalf("expected >= 6 network supports, got %d", len(supports))
	}

	seen := map[string]bool{}
	for _, item := range supports {
		row := item.(map[string]interface{})
		network := row["network"].(string)
		seen[network] = true
	}
	for _, n := range []string{"tron", "solana", "eth", "bsc", "polygon", "plasma"} {
		if !seen[n] {
			t.Fatalf("missing network support: %s", n)
		}
	}
}

func TestSupportedAssetCRUD(t *testing.T) {
	e := setupTestEnv(t)

	// public query list (no auth)
	req := httptest.NewRequest(http.MethodGet, "/payments/gmpay/v1/supported-assets/records", nil)
	rec := httptest.NewRecorder()
	e.ServeHTTP(rec, req)
	resp := parseResp(t, rec)
	if resp["status_code"].(float64) != 200 {
		t.Fatalf("public list failed: %v", resp)
	}

	// create requires auth
	rec = doPostWithToken(e, "/payments/gmpay/v1/supported-assets/add", map[string]interface{}{
		"network": "arb",
		"token":   "usdt",
		"status":  1,
	})
	resp = parseResp(t, rec)
	if resp["status_code"].(float64) != 200 {
		t.Fatalf("add supported asset failed: %v", resp)
	}
	created := resp["data"].(map[string]interface{})
	assetID := fmt.Sprintf("%.0f", created["id"].(float64))

	// get by id is public
	req = httptest.NewRequest(http.MethodGet, "/payments/gmpay/v1/supported-assets/"+assetID, nil)
	rec = httptest.NewRecorder()
	e.ServeHTTP(rec, req)
	resp = parseResp(t, rec)
	if resp["status_code"].(float64) != 200 {
		t.Fatalf("public get by id failed: %v", resp)
	}

	// update requires auth
	rec = doPostWithToken(e, "/payments/gmpay/v1/supported-assets/"+assetID+"/update", map[string]interface{}{
		"network": "arbitrum",
		"token":   "usdc",
		"status":  1,
	})
	resp = parseResp(t, rec)
	if resp["status_code"].(float64) != 200 {
		t.Fatalf("update supported asset failed: %v", resp)
	}
	updated := resp["data"].(map[string]interface{})
	if updated["network"] != "arbitrum" || updated["token"] != "USDC" {
		t.Fatalf("unexpected updated data: %v", updated)
	}

	// delete requires auth
	rec = doPostWithToken(e, "/payments/gmpay/v1/supported-assets/"+assetID+"/delete", nil)
	resp = parseResp(t, rec)
	if resp["status_code"].(float64) != 200 {
		t.Fatalf("delete supported asset failed: %v", resp)
	}
}

// TestWalletAddAndList tests adding wallets via API and listing them.
func TestWalletAddAndList(t *testing.T) {
	e := setupTestEnv(t)

	// Add a solana wallet
	rec := doPostWithToken(e, "/payments/gmpay/v1/wallet/add", map[string]interface{}{
		"network": "solana",
		"address": "NewSolWallet001",
	})
	t.Logf("Add: %s", rec.Body.String())
	resp := parseResp(t, rec)
	if resp["status_code"].(float64) != 200 {
		t.Fatalf("add wallet failed: %v", resp)
	}

	// Add a tron wallet
	rec = doPostWithToken(e, "/payments/gmpay/v1/wallet/add", map[string]interface{}{
		"network": "tron",
		"address": "NewTronWallet001",
	})
	resp = parseResp(t, rec)
	if resp["status_code"].(float64) != 200 {
		t.Fatalf("add tron wallet failed: %v", resp)
	}

	// List all wallets
	rec = doGet(e, "/payments/gmpay/v1/wallet/list")
	resp = parseResp(t, rec)
	wallets := resp["data"].([]interface{})
	// 2 seeded + 2 added = 4
	if len(wallets) != 4 {
		t.Fatalf("expected 4 wallets, got %d: %v", len(wallets), wallets)
	}

	// List by network
	rec = doGet(e, "/payments/gmpay/v1/wallet/list?network=solana")
	resp = parseResp(t, rec)
	wallets = resp["data"].([]interface{})
	if len(wallets) != 2 {
		t.Fatalf("expected 2 solana wallets, got %d", len(wallets))
	}

	rec = doGet(e, "/payments/gmpay/v1/wallet/list?network=tron")
	resp = parseResp(t, rec)
	wallets = resp["data"].([]interface{})
	if len(wallets) != 2 {
		t.Fatalf("expected 2 tron wallets, got %d", len(wallets))
	}
}

// TestWalletDuplicateRejected tests that adding the same network+address twice fails.
func TestWalletDuplicateRejected(t *testing.T) {
	e := setupTestEnv(t)

	body := map[string]interface{}{"network": "solana", "address": "DupWallet001"}
	rec := doPostWithToken(e, "/payments/gmpay/v1/wallet/add", body)
	resp := parseResp(t, rec)
	if resp["status_code"].(float64) != 200 {
		t.Fatalf("first add failed: %v", resp)
	}

	// Same network+address should fail
	rec = doPostWithToken(e, "/payments/gmpay/v1/wallet/add", body)
	resp = parseResp(t, rec)
	if resp["status_code"].(float64) == 200 {
		t.Fatal("expected duplicate to be rejected")
	}
	t.Logf("Duplicate rejected: %v", resp["message"])

	// Same address, different network should succeed
	rec = doPostWithToken(e, "/payments/gmpay/v1/wallet/add", map[string]interface{}{
		"network": "tron",
		"address": "DupWallet001",
	})
	resp = parseResp(t, rec)
	if resp["status_code"].(float64) != 200 {
		t.Fatalf("same address on different network should succeed: %v", resp)
	}
}

// TestWalletStatusAndDelete tests enable/disable/delete operations.
func TestWalletStatusAndDelete(t *testing.T) {
	e := setupTestEnv(t)

	// Add a wallet
	rec := doPostWithToken(e, "/payments/gmpay/v1/wallet/add", map[string]interface{}{
		"network": "solana",
		"address": "StatusTestWallet",
	})
	resp := parseResp(t, rec)
	wallet := resp["data"].(map[string]interface{})
	walletID := fmt.Sprintf("%.0f", wallet["id"].(float64))

	// Get wallet
	rec = doGet(e, "/payments/gmpay/v1/wallet/"+walletID)
	resp = parseResp(t, rec)
	if resp["status_code"].(float64) != 200 {
		t.Fatalf("get wallet failed: %v", resp)
	}

	// Disable wallet
	rec = doPostWithToken(e, "/payments/gmpay/v1/wallet/"+walletID+"/status", map[string]interface{}{
		"status": 2,
	})
	resp = parseResp(t, rec)
	if resp["status_code"].(float64) != 200 {
		t.Fatalf("disable wallet failed: %v", resp)
	}

	// Verify disabled — should not appear in available list
	rec = doGet(e, "/payments/gmpay/v1/wallet/list?network=solana")
	resp = parseResp(t, rec)
	wallets := resp["data"].([]interface{})
	for _, w := range wallets {
		wm := w.(map[string]interface{})
		if wm["address"] == "StatusTestWallet" && wm["status"].(float64) != 2 {
			t.Error("wallet should be disabled")
		}
	}

	// Delete wallet
	rec = doPostWithToken(e, "/payments/gmpay/v1/wallet/"+walletID+"/delete", nil)
	resp = parseResp(t, rec)
	if resp["status_code"].(float64) != 200 {
		t.Fatalf("delete wallet failed: %v", resp)
	}

	// Verify deleted
	rec = doGet(e, "/payments/gmpay/v1/wallet/"+walletID)
	resp = parseResp(t, rec)
	// Should return not found
	if resp["status_code"].(float64) == 200 {
		data := resp["data"].(map[string]interface{})
		if data["id"].(float64) > 0 {
			t.Error("wallet should be deleted")
		}
	}
}

// TestWalletAuthRequired tests that wallet APIs require auth token.
func TestWalletAuthRequired(t *testing.T) {
	e := setupTestEnv(t)

	// No auth header — should not return success
	req := httptest.NewRequest(http.MethodGet, "/payments/gmpay/v1/wallet/list", nil)
	rec := httptest.NewRecorder()
	e.ServeHTTP(rec, req)

	// The response should indicate auth failure (not 200 success)
	var resp map[string]interface{}
	if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
		// echo may return plain text error
		if rec.Code == http.StatusOK {
			t.Error("expected auth failure without token")
		}
		t.Logf("Auth rejected (non-JSON): status=%d body=%s", rec.Code, rec.Body.String())
		return
	}
	statusCode, _ := resp["status_code"].(float64)
	if statusCode == 200 {
		t.Error("expected auth failure without token")
	}
	t.Logf("Auth rejected: %v", resp)
}

// TestCreateOrderNetworkIsolation verifies tron and solana wallets don't mix.
func TestCreateOrderNetworkIsolation(t *testing.T) {
	e := setupTestEnv(t)

	// Try to create a solana order — should get solana address, not tron
	body := signBody(map[string]interface{}{
		"order_id":   fmt.Sprintf("test-isolation-%d", 1),
		"amount":     1.00,
		"token":      "usdt",
		"currency":   "cny",
		"network":    "solana",
		"notify_url": "http://localhost/notify",
	})
	rec := doPost(e, "/payments/gmpay/v1/order/create-transaction", body)

	var resp map[string]interface{}
	json.Unmarshal(rec.Body.Bytes(), &resp)

	data, ok := resp["data"].(map[string]interface{})
	if !ok {
		t.Fatalf("expected data, got: %v", resp)
	}
	if data["receive_address"] == "TTestTronAddress001" {
		t.Error("solana order should NOT get a tron address")
	}
	if data["receive_address"] != "SolTestAddress001" {
		t.Errorf("expected SolTestAddress001, got %v", data["receive_address"])
	}
}
