# app/tests/unit/storage/test_instrument_store.py

from __future__ import annotations

from decimal import Decimal
from typing import cast

import pytest

from src.market_data.acquisition.models.instrument import Instrument
from src.storage.exceptions import (
    InstrumentStoreError,
    StorageError,
)
from src.storage.instrument_store import (
    InMemoryInstrumentStore,
    InstrumentStoreProtocol,
)


def _instrument(
    *,
    symbol: str = "BTC/USD_LEVERAGE",
    name: str = "BTC/USD",
    base_asset: str = "BTC",
) -> Instrument:
    return Instrument(
        symbol=symbol,
        name=name,
        status="TRADING",
        base_asset=base_asset,
        quote_asset="USD",
        asset_type="CRYPTOCURRENCY",
        market_type="LEVERAGE",
        market_modes=("REGULAR",),
        order_types=("LIMIT", "MARKET", "STOP"),
        base_asset_precision=4,
        quote_asset_precision=4,
        tick_size=Decimal("0.05"),
        tick_value=Decimal("3878.86"),
        step_size=Decimal("0.0001"),
        min_qty=Decimal("0.0001"),
        max_qty=Decimal("1000"),
        min_notional=Decimal("1"),
        country=None,
        sector=None,
        industry=None,
        trading_hours=None,
    )


def test_in_memory_store_matches_protocol() -> None:
    store = InMemoryInstrumentStore()

    assert isinstance(
        store,
        InstrumentStoreProtocol,
    )


def test_get_returns_none_for_unknown_source() -> None:
    store = InMemoryInstrumentStore()

    assert store.get("dzengi") is None


def test_set_and_get_preserve_same_tuple_object() -> None:
    store = InMemoryInstrumentStore()
    instruments = (
        _instrument(),
    )

    store.set(
        "dzengi",
        instruments,
    )

    result = store.get("dzengi")

    assert result is instruments


def test_empty_tuple_is_distinct_from_cache_miss() -> None:
    store = InMemoryInstrumentStore()

    assert store.get("dzengi") is None

    store.set(
        "dzengi",
        (),
    )

    result = store.get("dzengi")

    assert result == ()
    assert result is not None


def test_different_sources_are_isolated() -> None:
    store = InMemoryInstrumentStore()

    dzengi_instruments = (
        _instrument(
            symbol="BTC/USD_LEVERAGE",
        ),
    )
    secondary_instruments = (
        _instrument(
            symbol="ETH/USD_LEVERAGE",
            name="ETH/USD",
            base_asset="ETH",
        ),
    )

    store.set(
        "dzengi",
        dzengi_instruments,
    )
    store.set(
        "secondary",
        secondary_instruments,
    )

    assert store.get("dzengi") is dzengi_instruments
    assert store.get("secondary") is secondary_instruments


def test_clear_removes_only_requested_source() -> None:
    store = InMemoryInstrumentStore()

    dzengi_instruments = (
        _instrument(
            symbol="BTC/USD_LEVERAGE",
        ),
    )
    secondary_instruments = (
        _instrument(
            symbol="ETH/USD_LEVERAGE",
            name="ETH/USD",
            base_asset="ETH",
        ),
    )

    store.set(
        "dzengi",
        dzengi_instruments,
    )
    store.set(
        "secondary",
        secondary_instruments,
    )

    store.clear("dzengi")

    assert store.get("dzengi") is None
    assert store.get("secondary") is secondary_instruments


def test_clear_without_source_removes_all_sources() -> None:
    store = InMemoryInstrumentStore()

    store.set(
        "dzengi",
        (
            _instrument(),
        ),
    )
    store.set(
        "secondary",
        (
            _instrument(
                symbol="ETH/USD_LEVERAGE",
                name="ETH/USD",
                base_asset="ETH",
            ),
        ),
    )

    store.clear()

    assert store.get("dzengi") is None
    assert store.get("secondary") is None


def test_set_replaces_existing_value() -> None:
    store = InMemoryInstrumentStore()

    first = (
        _instrument(
            symbol="BTC/USD_LEVERAGE",
        ),
    )
    second = (
        _instrument(
            symbol="ETH/USD_LEVERAGE",
            name="ETH/USD",
            base_asset="ETH",
        ),
    )

    store.set(
        "dzengi",
        first,
    )
    store.set(
        "dzengi",
        second,
    )

    assert store.get("dzengi") is second


def test_source_name_outer_spaces_are_normalized() -> None:
    store = InMemoryInstrumentStore()
    instruments = (
        _instrument(),
    )

    store.set(
        "  dzengi  ",
        instruments,
    )

    assert store.get("dzengi") is instruments
    assert store.get("  dzengi  ") is instruments


def test_source_name_case_is_preserved() -> None:
    store = InMemoryInstrumentStore()

    lowercase = (
        _instrument(
            symbol="BTC/USD_LEVERAGE",
        ),
    )
    uppercase = (
        _instrument(
            symbol="ETH/USD_LEVERAGE",
            name="ETH/USD",
            base_asset="ETH",
        ),
    )

    store.set(
        "dzengi",
        lowercase,
    )
    store.set(
        "DZENGI",
        uppercase,
    )

    assert store.get("dzengi") is lowercase
    assert store.get("DZENGI") is uppercase


@pytest.mark.parametrize(
    "source_name",
    [
        "",
        " ",
        "   ",
        "\t",
        "\n",
    ],
)
def test_get_rejects_empty_source_name(
    source_name: str,
) -> None:
    store = InMemoryInstrumentStore()

    with pytest.raises(
        InstrumentStoreError,
        match=r"Имя источника Instrument Store не должно быть пустым",
    ):
        store.get(source_name)


@pytest.mark.parametrize(
    "source_name",
    [
        "",
        " ",
        "   ",
        "\t",
        "\n",
    ],
)
def test_set_rejects_empty_source_name(
    source_name: str,
) -> None:
    store = InMemoryInstrumentStore()

    with pytest.raises(
        InstrumentStoreError,
        match=r"Имя источника Instrument Store не должно быть пустым",
    ):
        store.set(
            source_name,
            (),
        )


@pytest.mark.parametrize(
    "source_name",
    [
        "",
        " ",
        "   ",
        "\t",
        "\n",
    ],
)
def test_targeted_clear_rejects_empty_source_name(
    source_name: str,
) -> None:
    store = InMemoryInstrumentStore()

    with pytest.raises(
        InstrumentStoreError,
        match=r"Имя источника Instrument Store не должно быть пустым",
    ):
        store.clear(source_name)


def test_set_rejects_list_instead_of_tuple() -> None:
    store = InMemoryInstrumentStore()

    invalid_instruments = cast(
        tuple[Instrument, ...],
        [
            _instrument(),
        ],
    )

    with pytest.raises(
        InstrumentStoreError,
        match=r"должен быть передан как tuple",
    ):
        store.set(
            "dzengi",
            invalid_instruments,
        )


def test_set_rejects_non_instrument_item() -> None:
    store = InMemoryInstrumentStore()

    invalid_instruments = cast(
        tuple[Instrument, ...],
        (
            "BTC/USD_LEVERAGE",
        ),
    )

    with pytest.raises(
        InstrumentStoreError,
        match=r"не являющийся Instrument",
    ):
        store.set(
            "dzengi",
            invalid_instruments,
        )


def test_store_preserves_instrument_order() -> None:
    store = InMemoryInstrumentStore()

    instruments = (
        _instrument(
            symbol="BTC/USD_LEVERAGE",
        ),
        _instrument(
            symbol="ETH/USD_LEVERAGE",
            name="ETH/USD",
            base_asset="ETH",
        ),
        _instrument(
            symbol="XRP/USD_LEVERAGE",
            name="XRP/USD",
            base_asset="XRP",
        ),
    )

    store.set(
        "dzengi",
        instruments,
    )

    result = store.get("dzengi")

    assert result is not None
    assert [
        instrument.symbol
        for instrument in result
    ] == [
        "BTC/USD_LEVERAGE",
        "ETH/USD_LEVERAGE",
        "XRP/USD_LEVERAGE",
    ]


def test_store_does_not_modify_input_tuple() -> None:
    store = InMemoryInstrumentStore()

    instruments = (
        _instrument(
            symbol="BTC/USD_LEVERAGE",
        ),
        _instrument(
            symbol="ETH/USD_LEVERAGE",
            name="ETH/USD",
            base_asset="ETH",
        ),
    )

    original = instruments

    store.set(
        "dzengi",
        instruments,
    )

    assert instruments is original
    assert store.get("dzengi") is original


def test_store_instances_are_isolated() -> None:
    first_store = InMemoryInstrumentStore()
    second_store = InMemoryInstrumentStore()

    instruments = (
        _instrument(),
    )

    first_store.set(
        "dzengi",
        instruments,
    )

    assert first_store.get("dzengi") is instruments
    assert second_store.get("dzengi") is None


def test_clear_unknown_source_is_idempotent() -> None:
    store = InMemoryInstrumentStore()

    store.clear("dzengi")
    store.clear("dzengi")

    assert store.get("dzengi") is None


def test_instrument_store_error_inherits_storage_error() -> None:
    error = InstrumentStoreError(
        "Storage failure."
    )

    assert isinstance(
        error,
        StorageError,
    )
    assert str(error) == "Storage failure."