"""Tests for pyperliquidity.cli — config parsing, validation, env loading."""

from __future__ import annotations

import argparse
import textwrap
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from pyperliquidity.cli import (
    _build_ws_state,
    _cmd_grid,
    _config_to_toml,
    _fetch_mid_px,
    _load_config,
    _load_env,
    _validate_config,
)

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

VALID_CONFIG = {
    "market": {"coin": "PURR", "testnet": False},
    "strategy": {"n_orders": 10, "order_sz": 100.0, "start_px": 1.0},
    "allocation": {"allocated_token": 1000.0, "allocated_usdc": 500.0},
}


def _write_toml(tmp_path: Path, content: str) -> Path:
    p = tmp_path / "config.toml"
    p.write_text(textwrap.dedent(content))
    return p


# ---------------------------------------------------------------------------
# _load_config
# ---------------------------------------------------------------------------


class TestLoadConfig:
    def test_missing_file(self) -> None:
        with pytest.raises(SystemExit, match="Config file not found"):
            _load_config("/nonexistent/config.toml")

    def test_malformed_toml(self, tmp_path: Path) -> None:
        p = _write_toml(tmp_path, "[[invalid\n")
        with pytest.raises(SystemExit, match="Failed to parse"):
            _load_config(str(p))

    def test_valid_toml(self, tmp_path: Path) -> None:
        p = _write_toml(
            tmp_path,
            """\
            [market]
            coin = "PURR"
            """,
        )
        result = _load_config(str(p))
        assert result["market"]["coin"] == "PURR"


# ---------------------------------------------------------------------------
# _load_env
# ---------------------------------------------------------------------------


class TestLoadEnv:
    def test_missing_private_key(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.delenv("PYPERLIQUIDITY_PRIVATE_KEY", raising=False)
        monkeypatch.delenv("PYPERLIQUIDITY_WALLET", raising=False)
        with pytest.raises(SystemExit, match="PRIVATE_KEY"):
            _load_env()

    def test_empty_private_key(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.setenv("PYPERLIQUIDITY_PRIVATE_KEY", "  ")
        monkeypatch.setenv("PYPERLIQUIDITY_WALLET", "0xabc")
        with pytest.raises(SystemExit, match="PRIVATE_KEY"):
            _load_env()

    def test_missing_wallet(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.setenv("PYPERLIQUIDITY_PRIVATE_KEY", "0xdeadbeef")
        monkeypatch.delenv("PYPERLIQUIDITY_WALLET", raising=False)
        with pytest.raises(SystemExit, match="WALLET"):
            _load_env()

    def test_empty_wallet(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.setenv("PYPERLIQUIDITY_PRIVATE_KEY", "0xdeadbeef")
        monkeypatch.setenv("PYPERLIQUIDITY_WALLET", "")
        with pytest.raises(SystemExit, match="WALLET"):
            _load_env()

    def test_both_set(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.setenv("PYPERLIQUIDITY_PRIVATE_KEY", "0xdeadbeef")
        monkeypatch.setenv("PYPERLIQUIDITY_WALLET", "0xabc")
        pk, w = _load_env()
        assert pk == "0xdeadbeef"
        assert w == "0xabc"


# ---------------------------------------------------------------------------
# _validate_config
# ---------------------------------------------------------------------------


class TestValidateConfig:
    def test_valid_config_passes(self) -> None:
        result = _validate_config({**VALID_CONFIG})
        assert result["tuning"]["interval_s"] == 3.0

    def test_missing_coin(self) -> None:
        cfg = {**VALID_CONFIG, "market": {}}
        with pytest.raises(SystemExit, match="market.coin"):
            _validate_config(cfg)

    def test_negative_order_sz(self) -> None:
        cfg = {**VALID_CONFIG, "strategy": {**VALID_CONFIG["strategy"], "order_sz": -1}}
        with pytest.raises(SystemExit, match="order_sz"):
            _validate_config(cfg)

    def test_zero_n_orders(self) -> None:
        cfg = {**VALID_CONFIG, "strategy": {**VALID_CONFIG["strategy"], "n_orders": 0}}
        with pytest.raises(SystemExit, match="n_orders"):
            _validate_config(cfg)

    def test_missing_allocated_token(self) -> None:
        cfg = {**VALID_CONFIG, "allocation": {"allocated_usdc": 500.0}}
        with pytest.raises(SystemExit, match="allocated_token"):
            _validate_config(cfg)

    def test_negative_allocated_usdc(self) -> None:
        cfg = {**VALID_CONFIG, "allocation": {**VALID_CONFIG["allocation"], "allocated_usdc": -10}}
        with pytest.raises(SystemExit, match="allocated_usdc"):
            _validate_config(cfg)

    def test_defaults_applied(self) -> None:
        """Omitted [tuning] section should get defaults."""
        result = _validate_config({**VALID_CONFIG})
        tuning = result["tuning"]
        assert tuning["interval_s"] == 3.0
        assert tuning["dead_zone_bps"] == 5.0
        assert tuning["price_tolerance_bps"] == 1.0
        assert tuning["size_tolerance_pct"] == 1.0
        assert tuning["reconcile_every"] == 20
        assert tuning["min_notional"] == 0.0

    def test_custom_tuning_preserved(self) -> None:
        """Provided tuning values should not be overwritten."""
        cfg = {**VALID_CONFIG, "tuning": {"interval_s": 5.0, "dead_zone_bps": 10.0}}
        result = _validate_config(cfg)
        assert result["tuning"]["interval_s"] == 5.0
        assert result["tuning"]["dead_zone_bps"] == 10.0
        # Others get defaults
        assert result["tuning"]["reconcile_every"] == 20

    def test_missing_start_px(self) -> None:
        cfg = {**VALID_CONFIG, "strategy": {"n_orders": 10, "order_sz": 100.0}}
        with pytest.raises(SystemExit, match="start_px"):
            _validate_config(cfg)

    def test_zero_start_px(self) -> None:
        cfg = {**VALID_CONFIG, "strategy": {**VALID_CONFIG["strategy"], "start_px": 0}}
        with pytest.raises(SystemExit, match="start_px"):
            _validate_config(cfg)

    def test_negative_start_px(self) -> None:
        cfg = {**VALID_CONFIG, "strategy": {**VALID_CONFIG["strategy"], "start_px": -1.0}}
        with pytest.raises(SystemExit, match="start_px"):
            _validate_config(cfg)

    def test_active_levels_optional(self) -> None:
        """active_levels is optional — omitting it should pass validation."""
        result = _validate_config({**VALID_CONFIG})
        assert result["strategy"].get("active_levels") is None

    def test_active_levels_valid(self) -> None:
        """Positive integer active_levels passes validation."""
        cfg = {**VALID_CONFIG, "strategy": {**VALID_CONFIG["strategy"], "active_levels": 5}}
        result = _validate_config(cfg)
        assert result["strategy"]["active_levels"] == 5

    def test_active_levels_zero_rejected(self) -> None:
        """active_levels=0 is rejected."""
        cfg = {**VALID_CONFIG, "strategy": {**VALID_CONFIG["strategy"], "active_levels": 0}}
        with pytest.raises(SystemExit, match="active_levels"):
            _validate_config(cfg)

    def test_active_levels_negative_rejected(self) -> None:
        """Negative active_levels is rejected."""
        cfg = {**VALID_CONFIG, "strategy": {**VALID_CONFIG["strategy"], "active_levels": -1}}
        with pytest.raises(SystemExit, match="active_levels"):
            _validate_config(cfg)

    def test_multiple_errors_reported(self) -> None:
        """All validation errors should be reported at once."""
        cfg = {"market": {}, "strategy": {}, "allocation": {}}
        with pytest.raises(SystemExit, match="market.coin") as exc_info:
            _validate_config(cfg)
        msg = str(exc_info.value)
        assert "n_orders" in msg


# ---------------------------------------------------------------------------
# _build_ws_state — allocation passthrough
# ---------------------------------------------------------------------------


class TestBuildWsStateAllocation:
    """Verify that allocation config values are passed to WsState."""

    @patch("eth_account.Account")
    @patch("hyperliquid.exchange.Exchange")
    @patch("hyperliquid.info.Info")
    def test_allocation_passed_to_ws_state(
        self, mock_info_cls: MagicMock, mock_exchange_cls: MagicMock, mock_account_cls: MagicMock,
    ) -> None:
        mock_info_cls.return_value = MagicMock()
        mock_exchange_cls.return_value = MagicMock()
        mock_account_cls.from_key.return_value = MagicMock()

        config = _validate_config({**VALID_CONFIG})
        ws = _build_ws_state(config, private_key="0xdeadbeef", wallet="0xabc")

        assert ws._allocated_token == 1000.0
        assert ws._allocated_usdc == 500.0
        assert ws.active_levels is None

    @patch("eth_account.Account")
    @patch("hyperliquid.exchange.Exchange")
    @patch("hyperliquid.info.Info")
    def test_active_levels_passed_to_ws_state(
        self, mock_info_cls: MagicMock, mock_exchange_cls: MagicMock, mock_account_cls: MagicMock,
    ) -> None:
        mock_info_cls.return_value = MagicMock()
        mock_exchange_cls.return_value = MagicMock()
        mock_account_cls.from_key.return_value = MagicMock()

        cfg = {**VALID_CONFIG, "strategy": {**VALID_CONFIG["strategy"], "active_levels": 10}}
        config = _validate_config(cfg)
        ws = _build_ws_state(config, private_key="0xdeadbeef", wallet="0xabc")

        assert ws.active_levels == 10

    @patch("eth_account.Account")
    @patch("hyperliquid.exchange.Exchange")
    @patch("hyperliquid.info.Info")
    def test_cancel_on_shutdown_default(
        self, mock_info_cls: MagicMock, mock_exchange_cls: MagicMock, mock_account_cls: MagicMock,
    ) -> None:
        """cancel_on_shutdown defaults to True when not specified."""
        mock_info_cls.return_value = MagicMock()
        mock_exchange_cls.return_value = MagicMock()
        mock_account_cls.from_key.return_value = MagicMock()

        config = _validate_config({**VALID_CONFIG})
        ws = _build_ws_state(config, private_key="0xdeadbeef", wallet="0xabc")

        assert ws.cancel_on_shutdown is True

    @patch("eth_account.Account")
    @patch("hyperliquid.exchange.Exchange")
    @patch("hyperliquid.info.Info")
    def test_cancel_on_shutdown_false(
        self, mock_info_cls: MagicMock, mock_exchange_cls: MagicMock, mock_account_cls: MagicMock,
    ) -> None:
        """cancel_on_shutdown=False is passed through to WsState."""
        mock_info_cls.return_value = MagicMock()
        mock_exchange_cls.return_value = MagicMock()
        mock_account_cls.from_key.return_value = MagicMock()

        config = _validate_config({**VALID_CONFIG})
        ws = _build_ws_state(
            config, private_key="0xdeadbeef", wallet="0xabc", cancel_on_shutdown=False,
        )

        assert ws.cancel_on_shutdown is False


# ---------------------------------------------------------------------------
# --keep-orders flag parsing
# ---------------------------------------------------------------------------


class TestKeepOrdersFlag:
    """Verify --keep-orders CLI flag is parsed and passed through."""

    def test_keep_orders_flag_accepted(self) -> None:
        """--keep-orders flag is accepted by the argument parser."""
        import argparse

        parser = argparse.ArgumentParser(prog="pyperliquidity")
        sub = parser.add_subparsers(dest="command")
        run_parser = sub.add_parser("run")
        run_parser.add_argument("--config", required=True)
        run_parser.add_argument("--keep-orders", action="store_true", default=False)

        args = parser.parse_args(["run", "--config", "test.toml", "--keep-orders"])
        assert args.keep_orders is True

    def test_keep_orders_default_false(self) -> None:
        """--keep-orders defaults to False when not specified."""
        import argparse

        parser = argparse.ArgumentParser(prog="pyperliquidity")
        sub = parser.add_subparsers(dest="command")
        run_parser = sub.add_parser("run")
        run_parser.add_argument("--config", required=True)
        run_parser.add_argument("--keep-orders", action="store_true", default=False)

        args = parser.parse_args(["run", "--config", "test.toml"])
        assert args.keep_orders is False


# ---------------------------------------------------------------------------
# target_px config option
# ---------------------------------------------------------------------------


# Config with target_px instead of explicit allocations
TARGET_PX_CONFIG = {
    "market": {"coin": "PURR", "testnet": False},
    "strategy": {"n_orders": 10, "order_sz": 100.0, "start_px": 1.0, "target_px": 1.009},
}


class TestTargetPxValidation:
    def test_target_px_replaces_allocation_requirement(self) -> None:
        """When target_px is set, allocation section is not required."""
        result = _validate_config({**TARGET_PX_CONFIG})
        assert result["allocation"]["allocated_token"] > 0
        assert result["allocation"]["allocated_usdc"] > 0

    def test_target_px_computes_correct_allocations(self) -> None:
        """target_px auto-computes allocation that places cursor correctly."""
        cfg = {
            "market": {"coin": "PURR"},
            "strategy": {
                "n_orders": 20,
                "order_sz": 50.0,
                "start_px": 1.0,
                "target_px": 1.0,  # cursor at level 0 = all asks
            },
        }
        result = _validate_config(cfg)
        # cursor=0 means all 20 levels are asks
        assert result["allocation"]["allocated_token"] == 20 * 50.0
        assert result["allocation"]["allocated_usdc"] == 0.0

    def test_target_px_at_middle_of_grid(self) -> None:
        """target_px in the middle of the grid splits allocations."""
        from pyperliquidity.pricing_grid import PricingGrid

        grid = PricingGrid(start_px=1.0, n_orders=20)
        target = grid.levels[10]
        cfg = {
            "market": {"coin": "PURR"},
            "strategy": {
                "n_orders": 20,
                "order_sz": 50.0,
                "start_px": 1.0,
                "target_px": target,
            },
        }
        result = _validate_config(cfg)
        assert result["allocation"]["allocated_token"] == 10 * 50.0
        expected_usdc = sum(50.0 * grid.price_at_level(i) for i in range(10))
        assert abs(result["allocation"]["allocated_usdc"] - expected_usdc) < 1e-10

    def test_target_px_below_start_px_rejected(self) -> None:
        """target_px < start_px is rejected."""
        cfg = {
            "market": {"coin": "PURR"},
            "strategy": {
                "n_orders": 10,
                "order_sz": 100.0,
                "start_px": 1.0,
                "target_px": 0.5,
            },
        }
        with pytest.raises(SystemExit, match="target_px.*must be >= .*start_px"):
            _validate_config(cfg)

    def test_target_px_above_grid_rejected(self) -> None:
        """target_px above grid max is rejected."""
        cfg = {
            "market": {"coin": "PURR"},
            "strategy": {
                "n_orders": 10,
                "order_sz": 100.0,
                "start_px": 1.0,
                "target_px": 999.0,
            },
        }
        with pytest.raises(SystemExit, match="target_px"):
            _validate_config(cfg)

    def test_target_px_zero_rejected(self) -> None:
        """target_px=0 is rejected."""
        cfg = {
            "market": {"coin": "PURR"},
            "strategy": {
                "n_orders": 10,
                "order_sz": 100.0,
                "start_px": 1.0,
                "target_px": 0,
            },
        }
        with pytest.raises(SystemExit, match="target_px must be positive"):
            _validate_config(cfg)

    def test_target_px_negative_rejected(self) -> None:
        """Negative target_px is rejected."""
        cfg = {
            "market": {"coin": "PURR"},
            "strategy": {
                "n_orders": 10,
                "order_sz": 100.0,
                "start_px": 1.0,
                "target_px": -1.0,
            },
        }
        with pytest.raises(SystemExit, match="target_px must be positive"):
            _validate_config(cfg)

    def test_backward_compatible_without_target_px(self) -> None:
        """Without target_px, allocation fields are still required."""
        result = _validate_config({**VALID_CONFIG})
        assert result["allocation"]["allocated_token"] == 1000.0
        assert result["allocation"]["allocated_usdc"] == 500.0

    def test_no_target_px_missing_allocation_rejected(self) -> None:
        """Without target_px, missing allocations are still an error."""
        cfg = {
            "market": {"coin": "PURR"},
            "strategy": {"n_orders": 10, "order_sz": 100.0, "start_px": 1.0},
        }
        with pytest.raises(SystemExit, match="allocated_token"):
            _validate_config(cfg)

    def test_target_px_optional(self) -> None:
        """target_px is optional — omitting it should work with explicit allocations."""
        result = _validate_config({**VALID_CONFIG})
        # No target_px in the valid config
        assert result["strategy"].get("target_px") is None
        assert result["allocation"]["allocated_token"] == 1000.0


class TestBuildWsStateWithTargetPx:
    """Verify target_px computed allocations flow through to WsState."""

    @patch("eth_account.Account")
    @patch("hyperliquid.exchange.Exchange")
    @patch("hyperliquid.info.Info")
    def test_target_px_allocation_passed_to_ws_state(
        self, mock_info_cls: MagicMock, mock_exchange_cls: MagicMock, mock_account_cls: MagicMock,
    ) -> None:
        mock_info_cls.return_value = MagicMock()
        mock_exchange_cls.return_value = MagicMock()
        mock_account_cls.from_key.return_value = MagicMock()

        from pyperliquidity.pricing_grid import PricingGrid

        grid = PricingGrid(start_px=1.0, n_orders=10)
        target = grid.levels[5]
        cfg = {
            "market": {"coin": "PURR"},
            "strategy": {
                "n_orders": 10,
                "order_sz": 100.0,
                "start_px": 1.0,
                "target_px": target,
            },
        }
        config = _validate_config(cfg)
        ws = _build_ws_state(config, private_key="0xdeadbeef", wallet="0xabc")

        # cursor=5: 5 ask levels * 100 = 500 tokens
        assert ws._allocated_token == 500.0
        # 5 bid levels
        expected_usdc = sum(100.0 * grid.price_at_level(i) for i in range(5))
        assert abs(ws._allocated_usdc - expected_usdc) < 1e-10


# ---------------------------------------------------------------------------
# grid subcommand
# ---------------------------------------------------------------------------


class TestConfigToToml:
    def test_basic_output(self) -> None:
        config = {
            "market": {"coin": "@1434", "testnet": True},
            "strategy": {
                "n_orders": 100,
                "order_sz": 0.5,
                "start_px": 350,
                "target_px": 4000.0,
                "active_levels": 20,
            },
            "allocation": {"allocated_token": 40.0, "allocated_usdc": 1000.0},
            "tuning": {"min_notional": 10.0},
        }
        toml_str = _config_to_toml(config)
        assert '[market]' in toml_str
        assert 'coin = "@1434"' in toml_str
        assert "testnet = true" in toml_str
        assert "[strategy]" in toml_str
        assert "n_orders = 100" in toml_str
        assert "active_levels = 20" in toml_str
        assert "[allocation]" in toml_str
        assert "[tuning]" in toml_str

    def test_no_active_levels(self) -> None:
        config = {
            "market": {"coin": "TEST", "testnet": False},
            "strategy": {"n_orders": 10, "order_sz": 1.0, "start_px": 100, "target_px": 150.0},
            "allocation": {"allocated_token": 10.0, "allocated_usdc": 500.0},
            "tuning": {},
        }
        toml_str = _config_to_toml(config)
        assert "active_levels" not in toml_str


class TestGridSubcommand:
    def test_basic_stdout(self, capsys: pytest.CaptureFixture[str]) -> None:
        """Grid command outputs TOML to stdout."""
        args = argparse.Namespace(
            coin="@1434",
            price_range=[350, 50000],
            liquidity_token=40,
            target_px=4000.0,
            tick_size=0.003,
            active_levels=20,
            testnet=True,
            output=None,
            sz_decimals=None,
            min_notional=10.0,
        )
        _cmd_grid(args)
        captured = capsys.readouterr()
        assert "[market]" in captured.out
        assert "@1434" in captured.out
        assert "testnet = true" in captured.out
        # Summary on stderr
        assert "Grid config" in captured.err

    def test_output_to_file(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
        """--output writes TOML to a file instead of stdout."""
        out_file = tmp_path / "out.toml"
        args = argparse.Namespace(
            coin="TEST",
            price_range=[100, 200],
            liquidity_token=50,
            target_px=150.0,
            tick_size=0.003,
            active_levels=None,
            testnet=False,
            output=str(out_file),
            sz_decimals=None,
            min_notional=10.0,
        )
        _cmd_grid(args)
        captured = capsys.readouterr()
        assert out_file.exists()
        content = out_file.read_text()
        assert "[market]" in content
        # stdout should be empty (TOML goes to file)
        assert "[market]" not in captured.out
        # stderr should note the file was written
        assert "Config written to" in captured.err

    def test_invalid_range_exits(self) -> None:
        """min >= max should exit with error."""
        args = argparse.Namespace(
            coin="TEST",
            price_range=[200, 100],
            liquidity_token=50,
            target_px=150.0,
            tick_size=0.003,
            active_levels=None,
            testnet=False,
            output=None,
            sz_decimals=None,
            min_notional=10.0,
        )
        with pytest.raises(SystemExit, match="must be less than"):
            _cmd_grid(args)

    def test_summary_on_stderr(self, capsys: pytest.CaptureFixture[str]) -> None:
        """Summary info appears on stderr."""
        args = argparse.Namespace(
            coin="TEST",
            price_range=[100, 200],
            liquidity_token=50,
            target_px=150.0,
            tick_size=0.003,
            active_levels=10,
            testnet=False,
            output=None,
            sz_decimals=None,
            min_notional=10.0,
        )
        _cmd_grid(args)
        captured = capsys.readouterr()
        assert "Grid levels:" in captured.err
        assert "Order size:" in captured.err
        assert "Active lvls: 10" in captured.err

    @patch("pyperliquidity.cli._fetch_mid_px", return_value=508.0)
    def test_fetches_mid_px_when_no_target_px(
        self, mock_fetch: MagicMock, capsys: pytest.CaptureFixture[str],
    ) -> None:
        """When --target-px is omitted, fetch midPx from exchange."""
        args = argparse.Namespace(
            coin="@1434",
            price_range=[350, 50000],
            liquidity_token=40,
            target_px=None,
            tick_size=0.003,
            active_levels=20,
            testnet=True,
            output=None,
            sz_decimals=None,
            min_notional=10.0,
        )
        _cmd_grid(args)
        mock_fetch.assert_called_once_with("@1434", True)
        captured = capsys.readouterr()
        assert "508" in captured.err  # "Fetched mid price" message

    @patch("pyperliquidity.cli._fetch_mid_px")
    def test_target_px_skips_fetch(self, mock_fetch: MagicMock) -> None:
        """When --target-px is provided, no HTTP call is made."""
        args = argparse.Namespace(
            coin="@1434",
            price_range=[350, 50000],
            liquidity_token=40,
            target_px=500.0,
            tick_size=0.003,
            active_levels=20,
            testnet=True,
            output=None,
            sz_decimals=None,
            min_notional=10.0,
        )
        _cmd_grid(args)
        mock_fetch.assert_not_called()


class TestFetchMidPx:
    """Tests for _fetch_mid_px."""

    @patch("pyperliquidity.cli.requests.post")
    def test_returns_mid_price(self, mock_post: MagicMock) -> None:
        mock_resp = MagicMock()
        mock_resp.json.return_value = {"@1434": "508.5", "PURR": "0.01"}
        mock_resp.raise_for_status = MagicMock()
        mock_post.return_value = mock_resp

        result = _fetch_mid_px("@1434", testnet=False)
        assert result == 508.5
        # Verify it called mainnet URL
        call_args = mock_post.call_args
        assert "/info" in call_args[0][0]
        assert call_args[1]["json"] == {"type": "allMids"}

    @patch("pyperliquidity.cli.requests.post")
    def test_coin_not_found_exits(self, mock_post: MagicMock) -> None:
        mock_resp = MagicMock()
        mock_resp.json.return_value = {"PURR": "0.01"}
        mock_resp.raise_for_status = MagicMock()
        mock_post.return_value = mock_resp

        with pytest.raises(SystemExit, match="not found in allMids"):
            _fetch_mid_px("@9999", testnet=False)

    @patch("pyperliquidity.cli.requests.post", side_effect=Exception("connection refused"))
    def test_network_error_exits(self, mock_post: MagicMock) -> None:
        with pytest.raises(SystemExit, match="failed to fetch mid prices"):
            _fetch_mid_px("@1434", testnet=True)

    @patch("pyperliquidity.cli.requests.post")
    def test_testnet_uses_testnet_url(self, mock_post: MagicMock) -> None:
        mock_resp = MagicMock()
        mock_resp.json.return_value = {"@1434": "500.0"}
        mock_resp.raise_for_status = MagicMock()
        mock_post.return_value = mock_resp

        _fetch_mid_px("@1434", testnet=True)
        call_url = mock_post.call_args[0][0]
        assert "testnet" in call_url.lower()


class TestFloatFormatting:
    """Verify TOML output doesn't contain float noise."""

    def test_no_float_noise(self) -> None:
        config = {
            "market": {"coin": "@1434", "testnet": False},
            "strategy": {
                "n_orders": 100,
                "order_sz": 40.011300000000006,
                "start_px": 350.0,
                "target_px": 508.123456789,
            },
            "allocation": {
                "allocated_token": 39.999999999999993,
                "allocated_usdc": 1234.5678901234,
            },
            "tuning": {"min_notional": 10.0},
        }
        toml_str = _config_to_toml(config)
        # Should not contain the full noisy representation
        assert "40.011300000000006" not in toml_str
        assert "39.999999999999993" not in toml_str
        # Should contain clean values
        assert "40.0113" in toml_str
        assert "40" in toml_str  # allocated_token rounds cleanly

    def test_integer_like_floats_clean(self) -> None:
        config = {
            "market": {"coin": "TEST", "testnet": False},
            "strategy": {
                "n_orders": 10,
                "order_sz": 5.0,
                "start_px": 100.0,
                "target_px": 150.0,
            },
            "allocation": {"allocated_token": 50.0, "allocated_usdc": 500.0},
            "tuning": {},
        }
        toml_str = _config_to_toml(config)
        # g format strips trailing zeros: 5.0 -> "5", 100.0 -> "100"
        assert "order_sz = 5" in toml_str
        assert "start_px = 100" in toml_str
