package rest

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"testing"
)

type testNetworksCatalog struct {
	Name           string `json:"name"`
	DefaultChainID int    `json:"defaultChainId"`
	ExplorerURL    string `json:"explorerUrl"`
	TokenListURL   string `json:"tokenListUrl"`
	GeneratedBy    string `json:"generatedBy"`
	Chains         []struct {
		ChainID          string   `json:"chainId"`
		ChainIDDecimal   int      `json:"chainIdDecimal"`
		ChainName        string   `json:"chainName"`
		ShortName        string   `json:"shortName"`
		RPCURLs          []string `json:"rpcUrls"`
		BlockExplorerURL []string `json:"blockExplorerUrls"`
		InfoURL          string   `json:"infoURL"`
		ExplorerAPIURL   string   `json:"explorerApiUrl"`
		Testnet          bool     `json:"testnet"`
	} `json:"chains"`
}

type testTokenList struct {
	Name       string `json:"name"`
	Keywords   []string
	Extensions struct {
		DefaultChainID    int    `json:"defaultChainId"`
		ExplorerURL       string `json:"explorerUrl"`
		NetworksConfigURL string `json:"networksConfigUrl"`
	} `json:"extensions"`
	Tokens []struct {
		ChainID    int    `json:"chainId"`
		Address    string `json:"address"`
		Symbol     string `json:"symbol"`
		Decimals   int    `json:"decimals"`
		Extensions struct {
			UnitOfAccount   string `json:"unitOfAccount"`
			UnitDescription string `json:"unitDescription"`
		} `json:"extensions"`
	} `json:"tokens"`
}

type testCapabilitiesCatalog struct {
	Name        string `json:"name"`
	GeneratedBy string `json:"generatedBy"`
	ChainID     int    `json:"chainId"`
	ChainName   string `json:"chainName"`
	RPCURL      string `json:"rpcUrl"`
	HTTP        struct {
		SupportedMethods   []string `json:"supportedMethods"`
		UnsupportedMethods []string `json:"unsupportedMethods"`
		Notes              []string `json:"notes"`
	} `json:"http"`
	Tracing struct {
		SupportedMethods   []string `json:"supportedMethods"`
		UnsupportedMethods []string `json:"unsupportedMethods"`
		Notes              []string `json:"notes"`
	} `json:"tracing"`
	WalletSupport struct {
		WalletAddEthereumChain bool     `json:"walletAddEthereumChain"`
		WalletWatchAsset       bool     `json:"walletWatchAsset"`
		Notes                  []string `json:"notes"`
	} `json:"walletSupport"`
}

func setupConfigHandler() http.Handler {
	server := NewServer(nil, 138)
	mux := http.NewServeMux()
	server.SetupRoutes(mux)
	return server.addMiddleware(mux)
}

func containsString(items []string, want string) bool {
	for _, item := range items {
		if item == want {
			return true
		}
	}
	return false
}

func TestConfigNetworksEndpointProvidesWalletMetadata(t *testing.T) {
	handler := setupConfigHandler()
	req := httptest.NewRequest(http.MethodGet, "/api/config/networks", nil)
	w := httptest.NewRecorder()
	handler.ServeHTTP(w, req)

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

	if got := w.Header().Get("Access-Control-Allow-Origin"); got == "" {
		t.Fatal("expected CORS header on config endpoint")
	}

	var payload testNetworksCatalog
	if err := json.Unmarshal(w.Body.Bytes(), &payload); err != nil {
		t.Fatalf("failed to parse networks payload: %v", err)
	}

	if payload.DefaultChainID != 138 {
		t.Fatalf("expected defaultChainId 138, got %d", payload.DefaultChainID)
	}
	if payload.ExplorerURL == "" || payload.TokenListURL == "" || payload.GeneratedBy == "" {
		t.Fatal("expected root metadata fields to be populated")
	}
	if len(payload.Chains) < 3 {
		t.Fatalf("expected multiple chain entries, got %d", len(payload.Chains))
	}

	var foundChain138 bool
	for _, chain := range payload.Chains {
		if chain.ChainIDDecimal != 138 {
			continue
		}
		foundChain138 = true
		if chain.ShortName == "" || chain.InfoURL == "" || chain.ExplorerAPIURL == "" {
			t.Fatal("expected Chain 138 optional metadata to be populated")
		}
		if len(chain.RPCURLs) == 0 || len(chain.BlockExplorerURL) == 0 {
			t.Fatal("expected Chain 138 RPC and explorer URLs")
		}
		if chain.Testnet {
			t.Fatal("expected Chain 138 to be marked as mainnet")
		}
	}

	if !foundChain138 {
		t.Fatal("expected Chain 138 entry in networks catalog")
	}
}

func TestConfigTokenListEndpointProvidesOptionalMetadata(t *testing.T) {
	handler := setupConfigHandler()
	req := httptest.NewRequest(http.MethodGet, "/api/config/token-list", nil)
	w := httptest.NewRecorder()
	handler.ServeHTTP(w, req)

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

	var payload testTokenList
	if err := json.Unmarshal(w.Body.Bytes(), &payload); err != nil {
		t.Fatalf("failed to parse token list payload: %v", err)
	}

	if len(payload.Keywords) == 0 {
		t.Fatal("expected token list keywords")
	}
	if payload.Extensions.DefaultChainID != 138 || payload.Extensions.ExplorerURL == "" || payload.Extensions.NetworksConfigURL == "" {
		t.Fatal("expected root-level token list extensions")
	}

	var foundCXAUC bool
	var foundCUSDT bool
	for _, token := range payload.Tokens {
		switch token.Symbol {
		case "cXAUC":
			foundCXAUC = true
			if token.Extensions.UnitOfAccount == "" || token.Extensions.UnitDescription == "" {
				t.Fatal("expected cXAUC optional unit metadata")
			}
		case "cUSDT":
			foundCUSDT = true
			if token.Decimals != 6 {
				t.Fatalf("expected cUSDT decimals 6, got %d", token.Decimals)
			}
		}
	}

	if !foundCXAUC || !foundCUSDT {
		t.Fatal("expected cXAUC and cUSDT in token list")
	}
}

func TestConfigCapabilitiesEndpointProvidesRPCCapabilityMatrix(t *testing.T) {
	handler := setupConfigHandler()
	req := httptest.NewRequest(http.MethodGet, "/api/config/capabilities", nil)
	w := httptest.NewRecorder()
	handler.ServeHTTP(w, req)

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

	var payload testCapabilitiesCatalog
	if err := json.Unmarshal(w.Body.Bytes(), &payload); err != nil {
		t.Fatalf("failed to parse capabilities payload: %v", err)
	}

	if payload.ChainID != 138 || payload.ChainName == "" || payload.RPCURL == "" || payload.GeneratedBy == "" {
		t.Fatal("expected populated chain-level capability metadata")
	}
	if !payload.WalletSupport.WalletAddEthereumChain || !payload.WalletSupport.WalletWatchAsset {
		t.Fatal("expected wallet support flags to be true")
	}
	if !containsString(payload.HTTP.SupportedMethods, "eth_feeHistory") {
		t.Fatal("expected eth_feeHistory support to be documented")
	}
	if !containsString(payload.HTTP.SupportedMethods, "eth_maxPriorityFeePerGas") {
		t.Fatal("expected eth_maxPriorityFeePerGas support to be documented")
	}
	if !containsString(payload.Tracing.SupportedMethods, "trace_block") {
		t.Fatal("expected trace_block support to be documented")
	}
}

func TestConfigTokenListEndpointReloadsRuntimeFileWithoutRestart(t *testing.T) {
	dir := t.TempDir()
	file := filepath.Join(dir, "token-list.json")
	first := `{"name":"Runtime Token List v1","tokens":[{"chainId":138,"address":"0x1111111111111111111111111111111111111111","symbol":"RT1","name":"Runtime One","decimals":6}]}`
	second := `{"name":"Runtime Token List v2","tokens":[{"chainId":138,"address":"0x2222222222222222222222222222222222222222","symbol":"RT2","name":"Runtime Two","decimals":6}]}`

	if err := os.WriteFile(file, []byte(first), 0o644); err != nil {
		t.Fatalf("failed to write initial runtime file: %v", err)
	}
	t.Setenv("CONFIG_TOKEN_LIST_JSON_PATH", file)

	handler := setupConfigHandler()

	req1 := httptest.NewRequest(http.MethodGet, "/api/config/token-list", nil)
	w1 := httptest.NewRecorder()
	handler.ServeHTTP(w1, req1)

	if w1.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d", w1.Code)
	}
	if got := w1.Header().Get("X-Config-Source"); got != "runtime-file" {
		t.Fatalf("expected runtime-file config source, got %q", got)
	}
	etag1 := w1.Header().Get("ETag")
	if etag1 == "" {
		t.Fatal("expected ETag header on runtime-backed response")
	}

	var body1 testTokenList
	if err := json.Unmarshal(w1.Body.Bytes(), &body1); err != nil {
		t.Fatalf("failed to parse runtime token list v1: %v", err)
	}
	if body1.Name != "Runtime Token List v1" {
		t.Fatalf("expected first runtime payload, got %q", body1.Name)
	}

	if err := os.WriteFile(file, []byte(second), 0o644); err != nil {
		t.Fatalf("failed to write updated runtime file: %v", err)
	}

	req2 := httptest.NewRequest(http.MethodGet, "/api/config/token-list", nil)
	w2 := httptest.NewRecorder()
	handler.ServeHTTP(w2, req2)

	if w2.Code != http.StatusOK {
		t.Fatalf("expected 200 after runtime update, got %d", w2.Code)
	}
	if got := w2.Header().Get("ETag"); got == "" || got == etag1 {
		t.Fatalf("expected changed ETag after runtime update, got %q", got)
	}

	var body2 testTokenList
	if err := json.Unmarshal(w2.Body.Bytes(), &body2); err != nil {
		t.Fatalf("failed to parse runtime token list v2: %v", err)
	}
	if body2.Name != "Runtime Token List v2" {
		t.Fatalf("expected updated runtime payload, got %q", body2.Name)
	}
}

func TestConfigTokenListEndpointSupportsETagRevalidation(t *testing.T) {
	handler := setupConfigHandler()

	req1 := httptest.NewRequest(http.MethodGet, "/api/config/token-list", nil)
	w1 := httptest.NewRecorder()
	handler.ServeHTTP(w1, req1)

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

	etag := w1.Header().Get("ETag")
	if etag == "" {
		t.Fatal("expected ETag header")
	}

	req2 := httptest.NewRequest(http.MethodGet, "/api/config/token-list", nil)
	req2.Header.Set("If-None-Match", etag)
	w2 := httptest.NewRecorder()
	handler.ServeHTTP(w2, req2)

	if w2.Code != http.StatusNotModified {
		t.Fatalf("expected 304, got %d", w2.Code)
	}
}

func TestConfigEndpointsSupportOptionsPreflight(t *testing.T) {
	handler := setupConfigHandler()
	req := httptest.NewRequest(http.MethodOptions, "/api/config/token-list", nil)
	w := httptest.NewRecorder()
	handler.ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("expected 200 preflight response, got %d", w.Code)
	}
	if got := w.Header().Get("Access-Control-Allow-Methods"); got == "" {
		t.Fatal("expected Access-Control-Allow-Methods header")
	}
}
