"""
Unit tests for Pydantic schema validation.

Tests DeployConfig (terminal), UserCreate/UserLogin (auth),
InstanceControlRequest, OrderbookLevel, MarketOrderbook, InstanceStatusUpdate.
"""

import pytest
from pydantic import ValidationError

from app.modules.terminal.auto.schema import (
    DeployConfig,
    InstanceControlRequest,
    OrderbookLevel,
    MarketOrderbook,
    InstanceStatusUpdate,
    sanitize_string,
)
from app.modules.user.schema import UserCreate, UserLogin
from tests.conftest import VALID_UUID4, TEST_RSA_KEY


# ============================================================================
# sanitize_string
# ============================================================================


class TestSanitizeString:
    def test_clean_string(self):
        assert sanitize_string("HELLO") == "HELLO"

    def test_strips_whitespace(self):
        assert sanitize_string("  hello  ") == "hello"

    def test_empty_string(self):
        assert sanitize_string("") == ""

    def test_rejects_script_tag(self):
        with pytest.raises(ValueError, match="Invalid characters"):
            sanitize_string("<script>alert(1)</script>")

    def test_rejects_sql_injection(self):
        with pytest.raises(ValueError, match="Invalid characters"):
            sanitize_string("'; DROP TABLE users--")

    def test_rejects_javascript_uri(self):
        with pytest.raises(ValueError, match="Invalid characters"):
            sanitize_string("javascript:void(0)")

    def test_rejects_union_select(self):
        with pytest.raises(ValueError, match="Invalid characters"):
            sanitize_string("1 UNION SELECT * FROM users")


# ============================================================================
# DeployConfig — Kalshi
# ============================================================================


class TestDeployConfigKalshi:
    def test_valid_single_market(self, kalshi_deploy_config):
        config = DeployConfig(**kalshi_deploy_config)
        assert config.platform == "kalshi"
        assert config.num_markets == 1
        assert len(config.markets) == 1

    def test_valid_two_market(self, kalshi_2market_config):
        config = DeployConfig(**kalshi_2market_config)
        assert config.num_markets == 2
        assert len(config.markets) == 2
        assert config.both_side == "no"

    def test_rejects_invalid_platform(self, kalshi_deploy_config):
        kalshi_deploy_config["platform"] = "binance"
        with pytest.raises(ValidationError):
            DeployConfig(**kalshi_deploy_config)

    def test_rejects_missing_api_key(self, kalshi_deploy_config):
        kalshi_deploy_config["kalshi_api_key"] = None
        with pytest.raises(ValidationError, match="API key"):
            DeployConfig(**kalshi_deploy_config)

    def test_rejects_invalid_uuid_api_key(self, kalshi_deploy_config):
        kalshi_deploy_config["kalshi_api_key"] = "not-a-valid-uuid-at-all-1234567"
        with pytest.raises(ValidationError):
            DeployConfig(**kalshi_deploy_config)

    def test_rejects_missing_rsa_key(self, kalshi_deploy_config):
        kalshi_deploy_config["rsa_key_path"] = None
        with pytest.raises(ValidationError, match="RSA key"):
            DeployConfig(**kalshi_deploy_config)

    def test_rejects_short_rsa_key(self, kalshi_deploy_config):
        kalshi_deploy_config["rsa_key_path"] = "short"
        with pytest.raises(ValidationError):
            DeployConfig(**kalshi_deploy_config)

    def test_rejects_rsa_key_without_pem_markers(self, kalshi_deploy_config):
        kalshi_deploy_config["rsa_key_path"] = "a" * 200
        with pytest.raises(ValidationError, match="PEM format"):
            DeployConfig(**kalshi_deploy_config)

    def test_rejects_invalid_market_ticker(self, kalshi_deploy_config):
        kalshi_deploy_config["markets"] = ["INVALID"]
        with pytest.raises(ValidationError, match="ticker"):
            DeployConfig(**kalshi_deploy_config)

    def test_rejects_empty_market_ticker(self, kalshi_deploy_config):
        kalshi_deploy_config["markets"] = [""]
        with pytest.raises(ValidationError):
            DeployConfig(**kalshi_deploy_config)

    def test_rejects_duplicate_markets(self, kalshi_2market_config):
        kalshi_2market_config["markets"] = [
            "KXNBA-25MAR05-LALMIA-LAL",
            "KXNBA-25MAR05-LALMIA-LAL",
        ]
        with pytest.raises(ValidationError, match="Duplicate"):
            DeployConfig(**kalshi_2market_config)

    def test_market_count_mismatch(self, kalshi_deploy_config):
        kalshi_deploy_config["num_markets"] = 2
        with pytest.raises(ValidationError, match="exactly 2"):
            DeployConfig(**kalshi_deploy_config)

    def test_rejects_xss_in_market(self, kalshi_deploy_config):
        kalshi_deploy_config["markets"] = ["<script>alert(1)</script>"]
        with pytest.raises(ValidationError):
            DeployConfig(**kalshi_deploy_config)


# ============================================================================
# DeployConfig — Spreads & Bounds
# ============================================================================


class TestDeployConfigSpreads:
    def test_min_spread_equals_max_rejected(self, kalshi_deploy_config):
        kalshi_deploy_config["min_spread"] = 5
        kalshi_deploy_config["max_spread"] = 5
        with pytest.raises(ValidationError, match="greater than"):
            DeployConfig(**kalshi_deploy_config)

    def test_min_spread_greater_than_max_rejected(self, kalshi_deploy_config):
        kalshi_deploy_config["min_spread"] = 10
        kalshi_deploy_config["max_spread"] = 5
        with pytest.raises(ValidationError, match="greater than"):
            DeployConfig(**kalshi_deploy_config)

    def test_spread_range_too_wide(self, kalshi_deploy_config):
        kalshi_deploy_config["min_spread"] = 1
        kalshi_deploy_config["max_spread"] = 55
        with pytest.raises(ValidationError, match="too wide"):
            DeployConfig(**kalshi_deploy_config)

    def test_valid_m1_bounds(self, kalshi_deploy_config):
        config = DeployConfig(**kalshi_deploy_config)
        assert config.m1_bounds == [1, 7, 93, 99]

    def test_invalid_m1_bounds_overlap(self, kalshi_deploy_config):
        kalshi_deploy_config["m1_bounds"] = [1, 50, 40, 99]
        with pytest.raises(ValidationError, match="overlap"):
            DeployConfig(**kalshi_deploy_config)

    def test_invalid_m1_bounds_first_range(self, kalshi_deploy_config):
        kalshi_deploy_config["m1_bounds"] = [10, 5, 93, 99]
        with pytest.raises(ValidationError, match="First range"):
            DeployConfig(**kalshi_deploy_config)

    def test_invalid_m1_bounds_second_range(self, kalshi_deploy_config):
        kalshi_deploy_config["m1_bounds"] = [1, 7, 99, 93]
        with pytest.raises(ValidationError, match="Second range"):
            DeployConfig(**kalshi_deploy_config)

    def test_m1_bounds_out_of_range(self, kalshi_deploy_config):
        kalshi_deploy_config["m1_bounds"] = [0, 7, 93, 100]
        with pytest.raises(ValidationError, match="between 1 and 99"):
            DeployConfig(**kalshi_deploy_config)


# ============================================================================
# DeployConfig — Positions & Grid
# ============================================================================


class TestDeployConfigPositions:
    def test_position_increment_too_high(self, kalshi_deploy_config):
        kalshi_deploy_config["position_increment"] = 1001
        with pytest.raises(ValidationError):
            DeployConfig(**kalshi_deploy_config)

    def test_max_position_too_high(self, kalshi_deploy_config):
        kalshi_deploy_config["max_position"] = 100001
        with pytest.raises(ValidationError):
            DeployConfig(**kalshi_deploy_config)

    def test_valid_grid_levels(self, kalshi_deploy_config):
        kalshi_deploy_config["grid_mode"] = True
        kalshi_deploy_config["grid_levels"] = [[5, 10], [10, 20], [15, 30]]
        config = DeployConfig(**kalshi_deploy_config)
        assert len(config.grid_levels) == 3

    def test_grid_levels_without_grid_mode(self, kalshi_deploy_config):
        kalshi_deploy_config["grid_mode"] = False
        kalshi_deploy_config["grid_levels"] = [[5, 10]]
        with pytest.raises(ValidationError, match="grid_mode"):
            DeployConfig(**kalshi_deploy_config)

    def test_grid_level_invalid_structure(self, kalshi_deploy_config):
        kalshi_deploy_config["grid_mode"] = True
        kalshi_deploy_config["grid_levels"] = [[5]]  # missing size
        with pytest.raises(ValidationError, match="price, size"):
            DeployConfig(**kalshi_deploy_config)

    def test_contract_increment_limits(self, kalshi_deploy_config):
        kalshi_deploy_config["contract_increment"] = 31
        with pytest.raises(ValidationError):
            DeployConfig(**kalshi_deploy_config)


# ============================================================================
# DeployConfig — Strategy & Price Feed
# ============================================================================


class TestDeployConfigStrategy:
    def test_valid_trade_strategies(self, kalshi_deploy_config):
        for strategy in ("join_jump", "rolling_avg", "cdf"):
            kalshi_deploy_config["trade_strategy"] = strategy
            config = DeployConfig(**kalshi_deploy_config)
            assert config.trade_strategy == strategy

    def test_invalid_trade_strategy(self, kalshi_deploy_config):
        kalshi_deploy_config["trade_strategy"] = "scalp"
        with pytest.raises(ValidationError):
            DeployConfig(**kalshi_deploy_config)

    def test_valid_price_feeds(self, kalshi_deploy_config):
        for feed in ("poly_mid", "kalshi_mid", "pyth", "ccxt_cfbm"):
            kalshi_deploy_config["price_feed"] = feed
            config = DeployConfig(**kalshi_deploy_config)
            assert config.price_feed == feed

    def test_invalid_price_feed(self, kalshi_deploy_config):
        kalshi_deploy_config["price_feed"] = "binance"
        with pytest.raises(ValidationError):
            DeployConfig(**kalshi_deploy_config)

    def test_rolling_avg_window_range(self, kalshi_deploy_config):
        kalshi_deploy_config["rolling_avg_window"] = 301
        with pytest.raises(ValidationError):
            DeployConfig(**kalshi_deploy_config)

    def test_rolling_avg_spread_range(self, kalshi_deploy_config):
        kalshi_deploy_config["rolling_avg_spread"] = 100
        with pytest.raises(ValidationError):
            DeployConfig(**kalshi_deploy_config)


# ============================================================================
# DeployConfig — Two-market specific
# ============================================================================


class TestDeployConfigTwoMarket:
    def test_both_side_required_for_2market(self, kalshi_2market_config):
        kalshi_2market_config["both_side"] = None
        with pytest.raises(ValidationError, match="both_side"):
            DeployConfig(**kalshi_2market_config)

    def test_both_side_invalid_value(self, kalshi_2market_config):
        kalshi_2market_config["both_side"] = "maybe"
        with pytest.raises(ValidationError):
            DeployConfig(**kalshi_2market_config)

    def test_valid_market_priority(self, kalshi_2market_config):
        for mp in ("none", "market1", "market2", "expensive"):
            kalshi_2market_config["market_priority"] = mp
            config = DeployConfig(**kalshi_2market_config)
            assert config.market_priority == mp

    def test_invalid_market_priority(self, kalshi_2market_config):
        kalshi_2market_config["market_priority"] = "random"
        with pytest.raises(ValidationError, match="market_priority"):
            DeployConfig(**kalshi_2market_config)


# ============================================================================
# DeployConfig — Turbine
# ============================================================================


class TestDeployConfigTurbine:
    def test_valid_turbine(self, turbine_deploy_config):
        config = DeployConfig(**turbine_deploy_config)
        assert config.platform == "turbine"
        assert config.turbine_asset == "BTC"

    def test_turbine_missing_key(self, turbine_deploy_config):
        turbine_deploy_config["turbine_private_key"] = None
        with pytest.raises(ValidationError, match="private key"):
            DeployConfig(**turbine_deploy_config)

    def test_turbine_invalid_key_format(self, turbine_deploy_config):
        turbine_deploy_config["turbine_private_key"] = "not-hex-at-all" * 5
        with pytest.raises(ValidationError, match="hex"):
            DeployConfig(**turbine_deploy_config)

    def test_turbine_missing_asset(self, turbine_deploy_config):
        turbine_deploy_config["turbine_asset"] = None
        with pytest.raises(ValidationError, match="asset"):
            DeployConfig(**turbine_deploy_config)


# ============================================================================
# DeployConfig — Polymarket
# ============================================================================


class TestDeployConfigPolymarket:
    def test_valid_polymarket(self, polymarket_deploy_config):
        config = DeployConfig(**polymarket_deploy_config)
        assert config.platform == "polymarket"
        assert config.poly_market_type == "interval"

    def test_polymarket_missing_key(self, polymarket_deploy_config):
        polymarket_deploy_config["poly_private_key"] = None
        with pytest.raises(ValidationError, match="private key"):
            DeployConfig(**polymarket_deploy_config)

    def test_polymarket_missing_market_type(self, polymarket_deploy_config):
        polymarket_deploy_config["poly_market_type"] = None
        with pytest.raises(ValidationError, match="market type"):
            DeployConfig(**polymarket_deploy_config)


# ============================================================================
# InstanceControlRequest
# ============================================================================


class TestInstanceControlRequest:
    def test_valid_actions(self):
        for action in [
            "pause", "resume", "toggle_pause", "single_fire", "stop",
            "force_stop", "end", "cancel_orders",
            "accept_next_market", "decline_next_market",
        ]:
            req = InstanceControlRequest(action=action)
            assert req.action == action

    def test_invalid_action(self):
        with pytest.raises(ValidationError, match="action"):
            InstanceControlRequest(action="explode")


# ============================================================================
# OrderbookLevel
# ============================================================================


class TestOrderbookLevel:
    def test_valid_level(self):
        lvl = OrderbookLevel(price=50, size=100)
        assert lvl.price == 50

    def test_price_out_of_range(self):
        with pytest.raises(ValidationError):
            OrderbookLevel(price=0, size=10)
        with pytest.raises(ValidationError):
            OrderbookLevel(price=100, size=10)

    def test_negative_size(self):
        with pytest.raises(ValidationError):
            OrderbookLevel(price=50, size=-1)

    def test_size_too_large(self):
        with pytest.raises(ValidationError):
            OrderbookLevel(price=50, size=1_000_001)


# ============================================================================
# MarketOrderbook
# ============================================================================


class TestMarketOrderbook:
    def test_valid_orderbook(self):
        ob = MarketOrderbook(
            last_traded=50,
            volume=1000,
            bids=[OrderbookLevel(price=49, size=10)],
            asks=[OrderbookLevel(price=51, size=10)],
        )
        assert ob.last_traded == 50

    def test_invalid_side(self):
        with pytest.raises(ValidationError):
            MarketOrderbook(
                side="maybe",
                last_traded=50,
                volume=1000,
                bids=[],
                asks=[],
            )

    def test_side_normalized(self):
        ob = MarketOrderbook(
            side="YES",
            last_traded=50,
            volume=0,
            bids=[],
            asks=[],
        )
        assert ob.side == "yes"


# ============================================================================
# InstanceStatusUpdate
# ============================================================================


class TestInstanceStatusUpdate:
    def test_valid_status_update(self):
        update = InstanceStatusUpdate(
            id=1,
            status="running",
            position=10,
            pnl="+$5.00",
            orderbook={},
            current_increment={},
        )
        assert update.status == "running"

    def test_invalid_status(self):
        with pytest.raises(ValidationError):
            InstanceStatusUpdate(
                id=1,
                status="exploding",
                position=0,
                pnl="$0",
                orderbook={},
                current_increment={},
            )

    def test_position_limit(self):
        with pytest.raises(ValidationError):
            InstanceStatusUpdate(
                id=1,
                status="running",
                position=100001,
                pnl="$0",
                orderbook={},
                current_increment={},
            )


# ============================================================================
# UserCreate
# ============================================================================


class TestUserCreate:
    def test_valid_user(self):
        user = UserCreate(
            username="trader1",
            email="trader@test.com",
            password="Test1234!",
        )
        assert user.username == "trader1"

    def test_username_too_short(self):
        with pytest.raises(ValidationError):
            UserCreate(username="ab", email="t@t.com", password="Test1234!")

    def test_username_special_chars_rejected(self):
        with pytest.raises(ValidationError, match="letters, numbers"):
            UserCreate(username="trad er!", email="t@t.com", password="Test1234!")

    def test_password_no_uppercase(self):
        with pytest.raises(ValidationError, match="uppercase"):
            UserCreate(username="trader1", email="t@t.com", password="test1234!")

    def test_password_no_lowercase(self):
        with pytest.raises(ValidationError, match="lowercase"):
            UserCreate(username="trader1", email="t@t.com", password="TEST1234!")

    def test_password_no_digit(self):
        with pytest.raises(ValidationError, match="digit"):
            UserCreate(username="trader1", email="t@t.com", password="TestTest!")

    def test_password_no_special_char(self):
        with pytest.raises(ValidationError, match="special"):
            UserCreate(username="trader1", email="t@t.com", password="Test12345")

    def test_password_too_short(self):
        with pytest.raises(ValidationError):
            UserCreate(username="trader1", email="t@t.com", password="Ts1!")

    def test_invalid_email(self):
        with pytest.raises(ValidationError):
            UserCreate(username="trader1", email="not-email", password="Test1234!")

    def test_xss_in_username(self):
        with pytest.raises(ValidationError):
            UserCreate(
                username="<script>alert(1)",
                email="t@t.com",
                password="Test1234!",
            )


class TestUserLogin:
    def test_valid_login(self):
        login = UserLogin(username="trader1", password="whatever")
        assert login.username == "trader1"

    def test_xss_in_login_username(self):
        with pytest.raises(ValidationError, match="Invalid characters"):
            UserLogin(username="admin'; DROP TABLE", password="test")
