"""Contract tests: verify SDK covers the full OpenAPI schema."""

import sys
import re
import inspect
import unittest
from unittest.mock import patch, MagicMock

sys.path.insert(0, ".")
from vaultsfyi.client import VaultsSdk


# Mapping from OpenAPI path to SDK method name
PATH_MAP = {
    "/v2/health": "get_health",
    "/v2/vaults": "get_vaults",
    "/v2/assets": "get_assets",
    "/v2/networks": "get_networks",
    "/v2/tags": "get_tags",
    "/v2/benchmarks/{network}": "get_benchmarks",
    "/v2/historical-benchmarks/{network}": "get_historical_benchmarks",
    "/v2/detailed-vaults": "get_detailed_vaults",
    "/v2/detailed-vaults/{network}/{vaultAddress}": "get_vault",
    "/v2/detailed-vaults/{network}/{vaultAddress}/apy": "get_vault_apy_breakdown",
    "/v2/detailed-vaults/{network}/{vaultAddress}/tvl": "get_vault_tvl_breakdown",
    "/v2/historical/{network}/{vaultAddress}": "get_vault_historical_data",
    "/v2/historical/{network}/{vaultAddress}/apy": "get_vault_historical_apy",
    "/v2/historical/{network}/{vaultAddress}/tvl": "get_vault_historical_tvl",
    "/v2/historical/{network}/{vaultAddress}/sharePrice": "get_vault_historical_share_price",
    "/v2/portfolio/positions/{userAddress}": "get_positions",
    "/v2/portfolio/positions/{userAddress}/{network}/{vaultAddress}": "get_position",
    "/v2/portfolio/best-vault/{userAddress}": "get_best_vault",
    "/v2/portfolio/best-deposit-options/{userAddress}": "get_deposit_options",
    "/v2/portfolio/idle-assets/{userAddress}": "get_idle_assets",
    "/v2/portfolio/total-returns/{userAddress}/{network}/{vaultAddress}": "get_vault_total_returns",
    "/v2/portfolio/events/{userAddress}/{network}/{vaultAddress}": "get_vault_holder_events",
    "/v2/transactions/context/{userAddress}/{network}/{vaultAddress}": "get_transactions_context",
    "/v2/transactions/{action}/{userAddress}/{network}/{vaultAddress}": "get_actions",
    "/v2/transactions/rewards/context/{userAddress}": "get_rewards_context",
    "/v2/transactions/rewards/claim/{userAddress}": "get_rewards_claim",
    "/v2/curators": "get_curators",
    "/v2/historical/asset-prices/{network}/{assetAddress}": "get_historical_asset_prices",
}

PARAM_SYNONYMS = {
    "vaultAddress": "vault_address",
    "userAddress": "user_address",
    "assetAddress": "asset_address",
}


def _to_snake(name: str) -> str:
    s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
    return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()


class TestAllEndpointsMapped(unittest.TestCase):
    """Every OpenAPI path must have a corresponding SDK method."""

    def test_all_paths_have_methods(self):
        methods = {
            n
            for n, _ in inspect.getmembers(VaultsSdk, predicate=inspect.isfunction)
            if not n.startswith("_")
        }
        for path, method_name in PATH_MAP.items():
            with self.subTest(path=path):
                self.assertIn(
                    method_name,
                    methods,
                    f"SDK missing method {method_name} for {path}",
                )


class TestExplicitParams(unittest.TestCase):
    """Core query params must be explicit in method signatures (not kwargs-only)."""

    @classmethod
    def setUpClass(cls):
        cls.methods = {
            n: f
            for n, f in inspect.getmembers(VaultsSdk, predicate=inspect.isfunction)
            if not n.startswith("_")
        }

    def _resolve(self, sig, api_name):
        for candidate in [api_name, _to_snake(api_name), PARAM_SYNONYMS.get(api_name)]:
            if candidate and candidate in sig.parameters:
                return candidate
        return None

    def test_no_kwargs_only_params(self):
        """Simulate loading the OpenAPI spec and checking params.

        Since we can't fetch the spec in unit tests, we hardcode the
        critical params that MUST be explicit per method.
        """
        critical = {
            "get_detailed_vaults": [
                "page", "perPage", "allowedAssets", "allowedNetworks",
                "minTvl", "maxTvl", "minApy", "maxApy", "sortBy", "sortOrder",
            ],
            "get_positions": [
                "allowedAssets", "allowedNetworks", "minTvl", "sortBy", "apyInterval",
            ],
            "get_best_vault": [
                "allowedAssets", "allowedNetworks", "minTvl", "apyInterval",
            ],
            "get_deposit_options": [
                "allowedAssets", "allowedNetworks", "minTvl", "alwaysReturnAssets",
                "maxVaultsPerAsset",
            ],
            "get_idle_assets": [
                "allowedAssets", "allowedNetworks", "sortBy", "sortDirection",
            ],
            "get_vault_historical_data": [
                "page", "perPage", "apyInterval", "granularity",
                "fromTimestamp", "toTimestamp",
            ],
            "get_actions": ["all"],
            "get_rewards_claim": ["simulate", "claimIds"],
        }

        for method_name, params in critical.items():
            method = self.methods[method_name]
            sig = inspect.signature(method)
            for param in params:
                with self.subTest(method=method_name, param=param):
                    resolved = self._resolve(sig, param)
                    self.assertIsNotNone(
                        resolved,
                        f"{method_name} missing explicit param for {param}",
                    )


class TestBackwardCompat(unittest.TestCase):
    """get_all_vaults must still work as alias."""

    def test_get_all_vaults_is_alias(self):
        client = VaultsSdk(api_key="test")
        self.assertTrue(hasattr(client, "get_all_vaults"))
        # Should delegate to get_detailed_vaults
        with patch.object(client, "get_detailed_vaults", return_value={"data": []}) as mock:
            result = client.get_all_vaults(page=0, perPage=10)
            mock.assert_called_once_with(page=0, perPage=10)
            self.assertEqual(result, {"data": []})


class TestQuerySerialization(unittest.TestCase):
    """Verify params serialize correctly into query strings."""

    def setUp(self):
        self.client = VaultsSdk(api_key="test-key")

    @patch("vaultsfyi.client.requests.Session.request")
    def test_list_params(self, mock_req):
        mock_resp = MagicMock()
        mock_resp.ok = True
        mock_resp.status_code = 200
        mock_resp.json.return_value = {"data": []}
        mock_req.return_value = mock_resp

        self.client.get_detailed_vaults(
            allowed_assets=["USDC", "USDT"],
            allowed_networks=["mainnet", "base"],
        )

        call_url = mock_req.call_args[1]["url"]
        self.assertIn("allowedAssets=USDC", call_url)
        self.assertIn("allowedAssets=USDT", call_url)
        self.assertIn("allowedNetworks=mainnet", call_url)
        self.assertIn("allowedNetworks=base", call_url)

    @patch("vaultsfyi.client.requests.Session.request")
    def test_bool_params(self, mock_req):
        mock_resp = MagicMock()
        mock_resp.ok = True
        mock_resp.status_code = 200
        mock_resp.json.return_value = {"data": []}
        mock_req.return_value = mock_resp

        self.client.get_detailed_vaults(only_transactional=True)

        call_url = mock_req.call_args[1]["url"]
        self.assertIn("onlyTransactional=true", call_url)

    @patch("vaultsfyi.client.requests.Session.request")
    def test_optional_params_not_sent(self, mock_req):
        mock_resp = MagicMock()
        mock_resp.ok = True
        mock_resp.status_code = 200
        mock_resp.json.return_value = {"data": []}
        mock_req.return_value = mock_resp

        self.client.get_detailed_vaults(page=0)

        call_url = mock_req.call_args[1]["url"]
        self.assertIn("page=0", call_url)
        self.assertNotIn("minTvl", call_url)
        self.assertNotIn("allowedAssets", call_url)


if __name__ == "__main__":
    unittest.main()
