"""Unit tests for the fee_schedule_results resource — sync and async, success and error paths."""

from __future__ import annotations

import json

import httpx
import pytest

from rulebook import (
    AsyncRulebook,
    Rulebook,
    AuthenticationError,
    NotFoundError,
    PermissionDeniedError,
    InternalServerError,
)
from rulebook._response import APIResponse
from rulebook.types import FeeScheduleResult, FeeScheduleResultFilters, PaginatedResponse

BASE_URL = "https://api.rulebook.company/api/v1"

# ---------------------------------------------------------------------------
# Test data
# ---------------------------------------------------------------------------

RESULT_1 = {
    "id": "aaaaaaaa-1111-2222-3333-444444444444",
    "exchange_name": "CBOE",
    "fee_type": "Option",
    "fee_category": "Fee And Rebates",
    "fee_charge_type": "Per Contract",
    "fee_amount": "$0.25",
    "fee_action": "Make",
    "fee_action_details": None,
    "fee_basis": "Per Contract",
    "fee_notes": "Standard fee",
    "fee_reference": "Schedule A",
    "fee_participant": "Market Maker",
    "fee_participant_details": None,
    "fee_monthly_volume": "0-10000",
    "fee_monthly_volume_criteria": "Contracts",
    "fee_symbol_classification": "ETF",
    "fee_symbol_type": "Penny",
    "fee_trade_type": "Simple Order",
    "fee_symbol": None,
    "fee_excluded_symbols": None,
    "fee_conditions": None,
    "fee_exclusions": None,
    "fee_extra_info": None,
    "version_id": "bbbbbbbb-1111-2222-3333-444444444444",
    "scraped_time": "2025-06-01T12:00:00",
    "created_on": "2025-06-01T12:00:00",
}

RESULT_2 = {
    "id": "cccccccc-1111-2222-3333-444444444444",
    "exchange_name": "NYSE",
    "fee_type": "Equity",
    "fee_category": "Legal Regulatory Fees",
    "fee_charge_type": None,
    "fee_amount": "$0.10",
    "fee_action": "Take",
    "fee_action_details": None,
    "fee_basis": None,
    "fee_notes": None,
    "fee_reference": None,
    "fee_participant": "Customer",
    "fee_participant_details": None,
    "fee_monthly_volume": None,
    "fee_monthly_volume_criteria": None,
    "fee_symbol_classification": "Equity",
    "fee_symbol_type": "Non Penny",
    "fee_trade_type": "Complex Order",
    "fee_symbol": None,
    "fee_excluded_symbols": None,
    "fee_conditions": None,
    "fee_exclusions": None,
    "fee_extra_info": None,
    "version_id": "dddddddd-1111-2222-3333-444444444444",
    "scraped_time": "2025-06-01T12:00:00",
    "created_on": "2025-06-01T12:00:00",
}

PAGINATED_DATA = {
    "success": True,
    "message": "OK",
    "data": [RESULT_1, RESULT_2],
    "total_records": 50,
    "page_size": 20,
    "total_pages": 3,
    "current_page": 0,
    "query_time": 0.042,
}

FILTERS_DATA = {
    "exchange_names": ["CBOE", "NYSE"],
    "fee_types": ["Equity", "Option"],
    "fee_categories": ["Fee And Rebates", "Legal Regulatory Fees"],
    "fee_actions": ["Make", "Take"],
    "fee_participants": ["Market Maker", "Customer"],
    "fee_symbol_classifications": ["ETF", "Equity"],
    "fee_symbol_types": ["Penny", "Non Penny"],
    "fee_trade_types": ["Simple Order", "Complex Order"],
}


def _envelope(data):
    """Wrap data in the standard API response envelope."""
    return {"success": True, "message": "OK", "data": data}


def _error_body(message: str):
    """Build an error response body."""
    return {"success": False, "message": message}


# ===================================================================
# Sync — fee_schedule_results.list()
# ===================================================================


class TestFeeScheduleResultsList:
    """Tests for client.fee_schedule_results.list()."""

    def test_list_success(self, httpx_mock, client):
        httpx_mock.add_response(
            url=httpx.URL(f"{BASE_URL}/fee-schedule-results/", params={
                "order_by": "created_on",
                "order_dir": "desc",
                "page_size": "20",
                "page_number": "0",
            }),
            json=PAGINATED_DATA,
        )

        result = client.fee_schedule_results.list()

        assert isinstance(result, PaginatedResponse)
        assert result.total_records == 50
        assert result.page_size == 20
        assert result.total_pages == 3
        assert result.current_page == 0
        assert len(result.data) == 2
        assert isinstance(result.data[0], FeeScheduleResult)
        assert result.data[0].id == "aaaaaaaa-1111-2222-3333-444444444444"
        assert result.data[0].exchange_name == "CBOE"
        assert result.data[0].fee_type == "Option"
        assert result.data[0].fee_amount == "$0.25"
        assert result.data[1].exchange_name == "NYSE"

    def test_list_empty(self, httpx_mock, client):
        httpx_mock.add_response(
            json={
                "success": True,
                "message": "OK",
                "data": [],
                "total_records": 0,
                "page_size": 20,
                "total_pages": 0,
                "current_page": 0,
                "query_time": 0.001,
            },
        )

        result = client.fee_schedule_results.list()
        assert result.data == []
        assert result.total_records == 0

    def test_list_with_filters(self, httpx_mock, client):
        httpx_mock.add_response(json=PAGINATED_DATA)

        client.fee_schedule_results.list(
            exchange_name=["CBOE"],
            fee_type=["Option"],
            page_size=10,
            page_number=1,
        )

        request = httpx_mock.get_request()
        url = str(request.url)
        assert "exchange_name=CBOE" in url
        assert "fee_type=Option" in url
        assert "page_size=10" in url
        assert "page_number=1" in url

    def test_list_authentication_error(self, httpx_mock, client):
        httpx_mock.add_response(
            status_code=401,
            json=_error_body("Invalid API key"),
        )

        with pytest.raises(AuthenticationError) as exc_info:
            client.fee_schedule_results.list()

        assert exc_info.value.status_code == 401

    def test_list_server_error(self, httpx_mock, client):
        httpx_mock.add_response(
            status_code=500,
            json=_error_body("Internal server error"),
        )

        with pytest.raises(InternalServerError) as exc_info:
            client.fee_schedule_results.list()

        assert exc_info.value.status_code == 500


# ===================================================================
# Sync — fee_schedule_results.retrieve()
# ===================================================================


class TestFeeScheduleResultsRetrieve:
    """Tests for client.fee_schedule_results.retrieve(id)."""

    def test_retrieve_success(self, httpx_mock, client):
        result_id = "aaaaaaaa-1111-2222-3333-444444444444"
        httpx_mock.add_response(
            url=f"{BASE_URL}/fee-schedule-results/{result_id}",
            json=_envelope(RESULT_1),
        )

        result = client.fee_schedule_results.retrieve(result_id)

        assert isinstance(result, FeeScheduleResult)
        assert result.id == result_id
        assert result.exchange_name == "CBOE"
        assert result.fee_type == "Option"
        assert result.fee_category == "Fee And Rebates"
        assert result.fee_amount == "$0.25"
        assert result.fee_action == "Make"
        assert result.fee_participant == "Market Maker"
        assert result.fee_symbol_classification == "ETF"
        assert result.fee_symbol_type == "Penny"
        assert result.fee_trade_type == "Simple Order"
        assert result.version_id == "bbbbbbbb-1111-2222-3333-444444444444"

    def test_retrieve_not_found(self, httpx_mock, client):
        httpx_mock.add_response(
            url=f"{BASE_URL}/fee-schedule-results/nonexistent-id",
            status_code=404,
            json=_error_body("Fee schedule result not found"),
        )

        with pytest.raises(NotFoundError) as exc_info:
            client.fee_schedule_results.retrieve("nonexistent-id")

        assert exc_info.value.status_code == 404

    def test_retrieve_forbidden(self, httpx_mock, client):
        httpx_mock.add_response(
            url=f"{BASE_URL}/fee-schedule-results/restricted-id",
            status_code=403,
            json=_error_body("Access denied"),
        )

        with pytest.raises(PermissionDeniedError) as exc_info:
            client.fee_schedule_results.retrieve("restricted-id")

        assert exc_info.value.status_code == 403


# ===================================================================
# Sync — fee_schedule_results.get_filters()
# ===================================================================


class TestFeeScheduleResultsGetFilters:
    """Tests for client.fee_schedule_results.get_filters()."""

    def test_get_filters_success(self, httpx_mock, client):
        httpx_mock.add_response(
            url=f"{BASE_URL}/fee-schedule-results/filters",
            json=_envelope(FILTERS_DATA),
        )

        filters = client.fee_schedule_results.get_filters()

        assert isinstance(filters, FeeScheduleResultFilters)
        assert filters.exchange_names == ["CBOE", "NYSE"]
        assert filters.fee_types == ["Equity", "Option"]
        assert filters.fee_categories == ["Fee And Rebates", "Legal Regulatory Fees"]
        assert filters.fee_actions == ["Make", "Take"]
        assert filters.fee_participants == ["Market Maker", "Customer"]

    def test_get_filters_authentication_error(self, httpx_mock, client):
        httpx_mock.add_response(
            url=f"{BASE_URL}/fee-schedule-results/filters",
            status_code=401,
            json=_error_body("Invalid API key"),
        )

        with pytest.raises(AuthenticationError):
            client.fee_schedule_results.get_filters()


# ===================================================================
# Sync — fee_schedule_results.get_results_by_version()
# ===================================================================


class TestFeeScheduleResultsByVersion:
    """Tests for client.fee_schedule_results.get_results_by_version(version_id)."""

    def test_get_results_by_version_success(self, httpx_mock, client):
        version_id = "bbbbbbbb-1111-2222-3333-444444444444"
        httpx_mock.add_response(json=PAGINATED_DATA)

        result = client.fee_schedule_results.get_results_by_version(version_id)

        assert isinstance(result, PaginatedResponse)
        assert result.total_records == 50
        assert len(result.data) == 2

        request = httpx_mock.get_request()
        assert f"/fee-schedule-results/versions/{version_id}" in str(request.url)

    def test_get_results_by_version_not_found(self, httpx_mock, client):
        httpx_mock.add_response(
            status_code=404,
            json=_error_body("Version not found"),
        )

        with pytest.raises(NotFoundError):
            client.fee_schedule_results.get_results_by_version("nonexistent-version-id")

    def test_get_results_by_version_forbidden(self, httpx_mock, client):
        httpx_mock.add_response(
            status_code=403,
            json=_error_body("Access denied"),
        )

        with pytest.raises(PermissionDeniedError):
            client.fee_schedule_results.get_results_by_version("restricted-version-id")


# ===================================================================
# Sync — input validation
# ===================================================================


class TestFeeScheduleResultsValidation:
    """Tests for local input validation before HTTP calls."""

    def test_retrieve_empty_string_raises(self, client):
        with pytest.raises(ValueError, match="non-empty"):
            client.fee_schedule_results.retrieve("")

    def test_retrieve_none_raises(self, client):
        with pytest.raises((ValueError, TypeError)):
            client.fee_schedule_results.retrieve(None)  # type: ignore[arg-type]

    def test_get_results_by_version_empty_string_raises(self, client):
        with pytest.raises(ValueError, match="non-empty"):
            client.fee_schedule_results.get_results_by_version("")

    def test_get_results_by_version_none_raises(self, client):
        with pytest.raises((ValueError, TypeError)):
            client.fee_schedule_results.get_results_by_version(None)  # type: ignore[arg-type]


# ===================================================================
# Sync — with_raw_response
# ===================================================================


class TestFeeScheduleResultsWithRawResponse:
    """Tests for client.fee_schedule_results.with_raw_response wrappers."""

    def test_raw_response_list(self, httpx_mock, client):
        httpx_mock.add_response(json=PAGINATED_DATA)

        raw = client.fee_schedule_results.with_raw_response.list()

        assert isinstance(raw, APIResponse)
        assert raw.status_code == 200

        parsed = raw.parse()
        assert isinstance(parsed, PaginatedResponse)
        assert len(parsed.data) == 2
        assert isinstance(parsed.data[0], FeeScheduleResult)

    def test_raw_response_retrieve(self, httpx_mock, client):
        result_id = "aaaaaaaa-1111-2222-3333-444444444444"
        httpx_mock.add_response(
            url=f"{BASE_URL}/fee-schedule-results/{result_id}",
            json=_envelope(RESULT_1),
        )

        raw = client.fee_schedule_results.with_raw_response.retrieve(result_id)

        assert isinstance(raw, APIResponse)
        assert raw.status_code == 200

        result = raw.parse()
        assert isinstance(result, FeeScheduleResult)
        assert result.id == result_id

    def test_raw_response_get_filters(self, httpx_mock, client):
        httpx_mock.add_response(
            url=f"{BASE_URL}/fee-schedule-results/filters",
            json=_envelope(FILTERS_DATA),
        )

        raw = client.fee_schedule_results.with_raw_response.get_filters()

        assert isinstance(raw, APIResponse)
        assert raw.status_code == 200

        filters = raw.parse()
        assert isinstance(filters, FeeScheduleResultFilters)

    def test_raw_response_error_propagates(self, httpx_mock, client):
        httpx_mock.add_response(
            url=f"{BASE_URL}/fee-schedule-results/bad-id",
            status_code=404,
            json=_error_body("Not found"),
        )

        with pytest.raises(NotFoundError):
            client.fee_schedule_results.with_raw_response.retrieve("bad-id")

    def test_raw_response_empty_id_raises(self, client):
        with pytest.raises(ValueError, match="non-empty"):
            client.fee_schedule_results.with_raw_response.retrieve("")

    def test_cached_property(self, client):
        """with_raw_response is lazily cached — same instance on repeated access."""
        raw1 = client.fee_schedule_results.with_raw_response
        raw2 = client.fee_schedule_results.with_raw_response
        assert raw1 is raw2


# ===================================================================
# Async — fee_schedule_results.list()
# ===================================================================


class TestAsyncFeeScheduleResultsList:
    """Tests for async client.fee_schedule_results.list()."""

    @pytest.mark.asyncio
    async def test_list_success(self, httpx_mock, async_client):
        httpx_mock.add_response(json=PAGINATED_DATA)

        result = await async_client.fee_schedule_results.list()

        assert isinstance(result, PaginatedResponse)
        assert result.total_records == 50
        assert len(result.data) == 2
        assert isinstance(result.data[0], FeeScheduleResult)
        assert result.data[0].exchange_name == "CBOE"

    @pytest.mark.asyncio
    async def test_list_empty(self, httpx_mock, async_client):
        httpx_mock.add_response(
            json={
                "success": True,
                "message": "OK",
                "data": [],
                "total_records": 0,
                "page_size": 20,
                "total_pages": 0,
                "current_page": 0,
                "query_time": 0.001,
            },
        )

        result = await async_client.fee_schedule_results.list()
        assert result.data == []
        assert result.total_records == 0

    @pytest.mark.asyncio
    async def test_list_authentication_error(self, httpx_mock, async_client):
        httpx_mock.add_response(
            status_code=401,
            json=_error_body("Invalid API key"),
        )

        with pytest.raises(AuthenticationError) as exc_info:
            await async_client.fee_schedule_results.list()

        assert exc_info.value.status_code == 401


# ===================================================================
# Async — fee_schedule_results.retrieve()
# ===================================================================


class TestAsyncFeeScheduleResultsRetrieve:
    """Tests for async client.fee_schedule_results.retrieve(id)."""

    @pytest.mark.asyncio
    async def test_retrieve_success(self, httpx_mock, async_client):
        result_id = "aaaaaaaa-1111-2222-3333-444444444444"
        httpx_mock.add_response(
            url=f"{BASE_URL}/fee-schedule-results/{result_id}",
            json=_envelope(RESULT_1),
        )

        result = await async_client.fee_schedule_results.retrieve(result_id)

        assert isinstance(result, FeeScheduleResult)
        assert result.id == result_id
        assert result.exchange_name == "CBOE"

    @pytest.mark.asyncio
    async def test_retrieve_not_found(self, httpx_mock, async_client):
        httpx_mock.add_response(
            url=f"{BASE_URL}/fee-schedule-results/nonexistent-id",
            status_code=404,
            json=_error_body("Not found"),
        )

        with pytest.raises(NotFoundError):
            await async_client.fee_schedule_results.retrieve("nonexistent-id")

    @pytest.mark.asyncio
    async def test_retrieve_forbidden(self, httpx_mock, async_client):
        httpx_mock.add_response(
            url=f"{BASE_URL}/fee-schedule-results/restricted-id",
            status_code=403,
            json=_error_body("Access denied"),
        )

        with pytest.raises(PermissionDeniedError):
            await async_client.fee_schedule_results.retrieve("restricted-id")


# ===================================================================
# Async — fee_schedule_results.get_filters()
# ===================================================================


class TestAsyncFeeScheduleResultsGetFilters:
    """Tests for async client.fee_schedule_results.get_filters()."""

    @pytest.mark.asyncio
    async def test_get_filters_success(self, httpx_mock, async_client):
        httpx_mock.add_response(
            url=f"{BASE_URL}/fee-schedule-results/filters",
            json=_envelope(FILTERS_DATA),
        )

        filters = await async_client.fee_schedule_results.get_filters()

        assert isinstance(filters, FeeScheduleResultFilters)
        assert filters.exchange_names == ["CBOE", "NYSE"]
        assert filters.fee_types == ["Equity", "Option"]


# ===================================================================
# Async — fee_schedule_results.get_results_by_version()
# ===================================================================


class TestAsyncFeeScheduleResultsByVersion:
    """Tests for async client.fee_schedule_results.get_results_by_version(version_id)."""

    @pytest.mark.asyncio
    async def test_get_results_by_version_success(self, httpx_mock, async_client):
        version_id = "bbbbbbbb-1111-2222-3333-444444444444"
        httpx_mock.add_response(json=PAGINATED_DATA)

        result = await async_client.fee_schedule_results.get_results_by_version(version_id)

        assert isinstance(result, PaginatedResponse)
        assert result.total_records == 50
        assert len(result.data) == 2

    @pytest.mark.asyncio
    async def test_get_results_by_version_not_found(self, httpx_mock, async_client):
        httpx_mock.add_response(
            status_code=404,
            json=_error_body("Version not found"),
        )

        with pytest.raises(NotFoundError):
            await async_client.fee_schedule_results.get_results_by_version("nonexistent-id")


# ===================================================================
# Async — input validation
# ===================================================================


class TestAsyncFeeScheduleResultsValidation:
    """Async input validation tests."""

    @pytest.mark.asyncio
    async def test_retrieve_empty_string_raises(self, async_client):
        with pytest.raises(ValueError, match="non-empty"):
            await async_client.fee_schedule_results.retrieve("")

    @pytest.mark.asyncio
    async def test_get_results_by_version_empty_string_raises(self, async_client):
        with pytest.raises(ValueError, match="non-empty"):
            await async_client.fee_schedule_results.get_results_by_version("")


# ===================================================================
# Async — with_raw_response
# ===================================================================


class TestAsyncFeeScheduleResultsWithRawResponse:
    """Tests for async with_raw_response wrappers."""

    @pytest.mark.asyncio
    async def test_raw_response_list(self, httpx_mock, async_client):
        httpx_mock.add_response(json=PAGINATED_DATA)

        raw = await async_client.fee_schedule_results.with_raw_response.list()

        assert isinstance(raw, APIResponse)
        assert raw.status_code == 200

        parsed = raw.parse()
        assert isinstance(parsed, PaginatedResponse)
        assert len(parsed.data) == 2

    @pytest.mark.asyncio
    async def test_raw_response_retrieve(self, httpx_mock, async_client):
        result_id = "aaaaaaaa-1111-2222-3333-444444444444"
        httpx_mock.add_response(
            url=f"{BASE_URL}/fee-schedule-results/{result_id}",
            json=_envelope(RESULT_1),
        )

        raw = await async_client.fee_schedule_results.with_raw_response.retrieve(result_id)

        assert isinstance(raw, APIResponse)
        assert raw.status_code == 200

        result = raw.parse()
        assert isinstance(result, FeeScheduleResult)
        assert result.id == result_id


# ===================================================================
# Model tests
# ===================================================================


class TestFeeScheduleResultModel:
    """Tests for the FeeScheduleResult Pydantic model."""

    def test_from_dict(self):
        result = FeeScheduleResult.model_validate(RESULT_1)
        assert result.id == "aaaaaaaa-1111-2222-3333-444444444444"
        assert result.exchange_name == "CBOE"
        assert result.fee_type == "Option"
        assert result.fee_category == "Fee And Rebates"
        assert result.fee_amount == "$0.25"
        assert result.fee_action == "Make"
        assert result.fee_participant == "Market Maker"

    def test_optional_fields_none(self):
        minimal = {
            "id": "test-id",
            "exchange_name": "TEST",
            "fee_type": "Equity",
            "fee_category": "Test",
        }
        result = FeeScheduleResult.model_validate(minimal)
        assert result.fee_amount is None
        assert result.fee_action is None
        assert result.version_id is None

    def test_frozen(self):
        result = FeeScheduleResult.model_validate(RESULT_1)
        with pytest.raises(Exception):
            result.id = "MODIFIED"  # type: ignore[misc]


class TestFeeScheduleResultFiltersModel:
    """Tests for the FeeScheduleResultFilters Pydantic model."""

    def test_from_dict(self):
        filters = FeeScheduleResultFilters.model_validate(FILTERS_DATA)
        assert filters.exchange_names == ["CBOE", "NYSE"]
        assert filters.fee_types == ["Equity", "Option"]
        assert filters.fee_actions == ["Make", "Take"]

    def test_frozen(self):
        filters = FeeScheduleResultFilters.model_validate(FILTERS_DATA)
        with pytest.raises(Exception):
            filters.exchange_names = ["MODIFIED"]  # type: ignore[misc]


class TestPaginatedResponseModel:
    """Tests for the PaginatedResponse Pydantic model."""

    def test_from_dict(self):
        page = PaginatedResponse[FeeScheduleResult].model_validate({
            "data": [RESULT_1],
            "total_records": 1,
            "page_size": 20,
            "total_pages": 1,
            "current_page": 0,
            "query_time": 0.01,
        })
        assert page.total_records == 1
        assert page.page_size == 20
        assert len(page.data) == 1
        assert isinstance(page.data[0], FeeScheduleResult)
        assert page.data[0].exchange_name == "CBOE"

    def test_frozen(self):
        page = PaginatedResponse[FeeScheduleResult].model_validate({
            "data": [],
            "total_records": 0,
            "page_size": 20,
            "total_pages": 0,
            "current_page": 0,
        })
        with pytest.raises(Exception):
            page.total_records = 999  # type: ignore[misc]
