"""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,
    UnprocessableEntityError,
)
from rulebook._response import APIResponse
from rulebook.types import (
    FeeScheduleResult,
    FeeScheduleResultFilters,
    PaginatedResponse,
    Footnote,
    Definition,
    Relationships,
)

BASE_URL = "https://api.rulebookcompany.com/api/v1"

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

RESULT_1 = {
    "record_id": "aaaaaaaa-1111-2222-3333-444444444444",
    "exchange_name": "CBOE",
    "fee_type": "option",
    "fee_category": "fees_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_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_WITH_SOURCE = {
    **RESULT_1,
    "source_document": "CBOE US Options Fee Schedule",
    "source_link": "https://cdn.example.com/cboe-fee-schedule.pdf",
    "page_number": 3,
}

RESULT_WITH_RELATIONSHIPS = {
    **RESULT_1,
    "relationships": {
        "footnotes": [
            {"text": "Applies only to different contracts."},
            {"text": "See Section 5 for volume tiers."},
        ],
        "definitions": [
            {"term": "QCC", "text": "Qualified Contingent Cross"},
            {"term": "ADV", "text": "Average Daily Volume"},
        ],
    },
}

RESULT_WITH_ALL_INCLUDES = {
    **RESULT_1,
    "source_document": "CBOE US Options Fee Schedule",
    "source_link": "https://cdn.example.com/cboe-fee-schedule.pdf",
    "page_number": 3,
    "relationships": {
        "footnotes": [{"text": "Footnote 1"}],
        "definitions": [{"term": "Term1", "text": "Definition 1"}],
    },
}

RESULT_2 = {
    "record_id": "cccccccc-1111-2222-3333-444444444444",
    "exchange_name": "NYSE",
    "fee_type": "equity",
    "fee_category": "legal_regulatory_fee",
    "fee_charge_type": "Flat Fee",
    "fee_amount": "$0.10",
    "fee_action": "take",
    "fee_action_details": None,
    "fee_basis": None,
    "fee_notes": None,
    "fee_participant": "customer",
    "fee_participant_details": None,
    "fee_monthly_volume": None,
    "fee_monthly_volume_criteria": None,
    "fee_symbol_classification": "equity_class",
    "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 = {
    "data": [RESULT_1, RESULT_2],
    "meta": {
        "record_count": 50,
        "page_size": 20,
        "total_pages": 3,
        "current_page": 0,
        "query_time_ms": 42,
    },
}

PAGINATED_DATA_WITH_INCLUDES = {
    "data": [RESULT_WITH_ALL_INCLUDES],
    "meta": {
        "record_count": 1,
        "page_size": 20,
        "total_pages": 1,
        "current_page": 0,
        "query_time_ms": 42,
    },
}

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 {"data": data, "meta": {"record_count": 1, "query_time_ms": 5}}


def _error_body(code: str, message: str):
    """Build an error response body matching the API error envelope."""
    return {"error": {"code": code, "message": message}, "meta": {}}


# ===================================================================
# 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].record_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={
                "data": [],
                "meta": {
                    "record_count": 0,
                    "page_size": 20,
                    "total_pages": 0,
                    "current_page": 0,
                    "query_time_ms": 1,
                },
            },
        )

        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_with_include_params(self, httpx_mock, client):
        httpx_mock.add_response(json=PAGINATED_DATA_WITH_INCLUDES)

        result = client.fee_schedule_results.list(
            include_footnotes=True,
            include_definitions=True,
            include_source=True,
        )

        request = httpx_mock.get_request()
        url = str(request.url)
        assert "include_footnotes=true" in url.lower()
        assert "include_definitions=true" in url.lower()
        assert "include_source=true" in url.lower()

        assert len(result.data) == 1
        item = result.data[0]
        assert item.source_document == "CBOE US Options Fee Schedule"
        assert item.source_link == "https://cdn.example.com/cboe-fee-schedule.pdf"
        assert item.page_number == 3
        assert item.relationships is not None
        assert len(item.relationships.footnotes) == 1
        assert len(item.relationships.definitions) == 1

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

        client.fee_schedule_results.list()

        request = httpx_mock.get_request()
        url = str(request.url)
        assert "include_footnotes" not in url
        assert "include_definitions" not in url
        assert "include_source" not in url

    def test_list_authentication_error(self, httpx_mock, client):
        httpx_mock.add_response(
            status_code=401,
            json=_error_body("UNAUTHORIZED", "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", "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.record_id == result_id
        assert result.exchange_name == "CBOE"
        assert result.fee_type == "option"
        assert result.fee_category == "fees_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_with_include_params(self, httpx_mock, client):
        result_id = "aaaaaaaa-1111-2222-3333-444444444444"
        httpx_mock.add_response(json=_envelope(RESULT_WITH_ALL_INCLUDES))

        result = client.fee_schedule_results.retrieve(
            result_id,
            include_footnotes=True,
            include_definitions=True,
            include_source=True,
        )

        request = httpx_mock.get_request()
        url = str(request.url)
        assert "include_footnotes=true" in url.lower()
        assert "include_definitions=true" in url.lower()
        assert "include_source=true" in url.lower()

        assert result.source_document == "CBOE US Options Fee Schedule"
        assert result.page_number == 3
        assert result.relationships is not None
        assert result.relationships.footnotes[0].text == "Footnote 1"
        assert result.relationships.definitions[0].term == "Term1"

    def test_retrieve_without_include_params_omits_them(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),
        )

        client.fee_schedule_results.retrieve(result_id)

        request = httpx_mock.get_request()
        url = str(request.url)
        assert "include_footnotes" not in url
        assert "include_definitions" not in url
        assert "include_source" not in url

    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("NOT_FOUND", "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("FORBIDDEN", "Access denied"),
        )

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

        assert exc_info.value.status_code == 403

    def test_retrieve_url_encodes_id_with_special_chars(self, httpx_mock, client):
        httpx_mock.add_response(
            json=_envelope(RESULT_1),
        )

        client.fee_schedule_results.retrieve("id/with spaces")

        request = httpx_mock.get_request()
        url = str(request.url)
        assert "/fee-schedule-results/id%2Fwith%20spaces" in url


# ===================================================================
# 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_with_exchange_name(self, httpx_mock, client):
        httpx_mock.add_response(json=_envelope(FILTERS_DATA))

        client.fee_schedule_results.get_filters(
            exchange_name=["fee_cboe_us_options", "fee_nyse_arca_options"]
        )

        request = httpx_mock.get_request()
        url = str(request.url)
        assert "exchange_name=fee_cboe_us_options" in url
        assert "exchange_name=fee_nyse_arca_options" in url

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

        client.fee_schedule_results.get_filters()

        request = httpx_mock.get_request()
        url = str(request.url)
        assert "exchange_name" not in url

    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("UNAUTHORIZED", "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_with_include_params(self, httpx_mock, client):
        version_id = "bbbbbbbb-1111-2222-3333-444444444444"
        httpx_mock.add_response(json=PAGINATED_DATA_WITH_INCLUDES)

        result = client.fee_schedule_results.get_results_by_version(
            version_id,
            include_footnotes=True,
            include_definitions=True,
            include_source=True,
        )

        request = httpx_mock.get_request()
        url = str(request.url)
        assert "include_footnotes=true" in url.lower()
        assert "include_definitions=true" in url.lower()
        assert "include_source=true" in url.lower()

        assert len(result.data) == 1
        item = result.data[0]
        assert item.source_document == "CBOE US Options Fee Schedule"
        assert item.relationships is not None

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

        client.fee_schedule_results.get_results_by_version(version_id)

        request = httpx_mock.get_request()
        url = str(request.url)
        assert "include_footnotes" not in url
        assert "include_definitions" not in url
        assert "include_source" not in url

    def test_get_results_by_version_not_found(self, httpx_mock, client):
        httpx_mock.add_response(
            status_code=404,
            json=_error_body("NOT_FOUND", "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("FORBIDDEN", "Access denied"),
        )

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

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

        client.fee_schedule_results.get_results_by_version("id/with spaces")

        request = httpx_mock.get_request()
        url = str(request.url)
        assert "/fee-schedule-results/versions/id%2Fwith%20spaces" in url


# ===================================================================
# Sync — error_code and details on APIStatusError
# ===================================================================


class TestFeeScheduleResultsErrorFields:
    """Tests that error_code and details are surfaced on APIStatusError."""

    def test_error_code_surfaced_on_422(self, httpx_mock, client):
        httpx_mock.add_response(
            status_code=422,
            json={
                "error": {
                    "code": "VALIDATION_ERROR",
                    "message": "page_size: Input should be >= 1",
                    "details": [{"field": "page_size", "issue": "Input should be >= 1"}],
                },
                "meta": {},
            },
        )

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

        assert exc_info.value.error_code == "VALIDATION_ERROR"
        assert exc_info.value.details is not None
        assert len(exc_info.value.details) == 1
        assert exc_info.value.details[0]["field"] == "page_size"

    def test_error_code_surfaced_on_403_with_details(self, httpx_mock, client):
        httpx_mock.add_response(
            status_code=403,
            json={
                "error": {
                    "code": "FORBIDDEN",
                    "message": "You do not have access to the requested exchange(s).",
                    "details": [{"field": "exchange_name", "issue": "Not authorized: ['nasdaqphlx']"}],
                },
                "meta": {},
            },
        )

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

        assert exc_info.value.error_code == "FORBIDDEN"
        assert exc_info.value.details is not None
        assert exc_info.value.details[0]["field"] == "exchange_name"

    def test_error_code_none_for_legacy_format(self, httpx_mock, client):
        httpx_mock.add_response(
            status_code=500,
            json={"message": "Internal server error"},
        )

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

        assert exc_info.value.error_code is None
        assert exc_info.value.details is None

    def test_error_code_on_raw_response_404(self, httpx_mock, client):
        httpx_mock.add_response(
            url=f"{BASE_URL}/fee-schedule-results/bad-id",
            status_code=404,
            json={
                "error": {"code": "NOT_FOUND", "message": "Not found"},
                "meta": {},
            },
        )

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

        assert exc_info.value.error_code == "NOT_FOUND"


# ===================================================================
# 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.record_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_get_results_by_version(self, httpx_mock, client):
        version_id = "bbbbbbbb-1111-2222-3333-444444444444"
        httpx_mock.add_response(json=PAGINATED_DATA)

        raw = client.fee_schedule_results.with_raw_response.get_results_by_version(version_id)

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

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

    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", "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={
                "data": [],
                "meta": {
                    "record_count": 0,
                    "page_size": 20,
                    "total_pages": 0,
                    "current_page": 0,
                    "query_time_ms": 1,
                },
            },
        )

        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("UNAUTHORIZED", "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.record_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", "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("FORBIDDEN", "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"]

    @pytest.mark.asyncio
    async def test_get_filters_with_exchange_name(self, httpx_mock, async_client):
        httpx_mock.add_response(json=_envelope(FILTERS_DATA))

        await async_client.fee_schedule_results.get_filters(
            exchange_name=["fee_cboe_us_options"]
        )

        request = httpx_mock.get_request()
        url = str(request.url)
        assert "exchange_name=fee_cboe_us_options" in url


# ===================================================================
# 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_with_include_params(self, httpx_mock, async_client):
        version_id = "bbbbbbbb-1111-2222-3333-444444444444"
        httpx_mock.add_response(json=PAGINATED_DATA_WITH_INCLUDES)

        result = await async_client.fee_schedule_results.get_results_by_version(
            version_id,
            include_footnotes=True,
            include_definitions=True,
            include_source=True,
        )

        request = httpx_mock.get_request()
        url = str(request.url)
        assert "include_footnotes=true" in url.lower()
        assert "include_definitions=true" in url.lower()
        assert "include_source=true" in url.lower()

        assert len(result.data) == 1
        assert result.data[0].source_document == "CBOE US Options Fee Schedule"

    @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("NOT_FOUND", "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.record_id == result_id

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

        raw = await async_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)

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

        raw = await async_client.fee_schedule_results.with_raw_response.get_results_by_version(version_id)

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

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


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


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

    def test_from_dict(self):
        result = FeeScheduleResult.model_validate(RESULT_1)
        assert result.record_id == "aaaaaaaa-1111-2222-3333-444444444444"
        assert result.exchange_name == "CBOE"
        assert result.fee_type == "option"
        assert result.fee_category == "fees_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 = {
            "record_id": "test-id",
            "exchange_name": "TEST",
            "fee_type": "equity",
            "fee_category": "fees_and_rebates",
            "fee_charge_type": "Per Contract",
            "fee_amount": "$0.00",
            "fee_action": "make",
            "fee_participant": "customer",
            "version_id": "eeeeeeee-1111-2222-3333-444444444444",
            "scraped_time": "2025-06-01T12:00:00",
        }
        result = FeeScheduleResult.model_validate(minimal)
        assert result.fee_basis is None
        assert result.fee_notes is None
        assert result.fee_symbol is None

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


class TestFeeScheduleResultModelWithSource:
    """Tests for FeeScheduleResult with source fields."""

    def test_source_fields_populated(self):
        result = FeeScheduleResult.model_validate(RESULT_WITH_SOURCE)
        assert result.source_document == "CBOE US Options Fee Schedule"
        assert result.source_link == "https://cdn.example.com/cboe-fee-schedule.pdf"
        assert result.page_number == 3

    def test_source_fields_default_none(self):
        result = FeeScheduleResult.model_validate(RESULT_1)
        assert result.source_document is None
        assert result.source_link is None
        assert result.page_number is None


class TestFeeScheduleResultModelWithRelationships:
    """Tests for FeeScheduleResult with relationships."""

    def test_relationships_populated(self):
        result = FeeScheduleResult.model_validate(RESULT_WITH_RELATIONSHIPS)
        assert result.relationships is not None
        assert len(result.relationships.footnotes) == 2
        assert len(result.relationships.definitions) == 2

        assert result.relationships.footnotes[0].text == "Applies only to different contracts."
        assert result.relationships.footnotes[1].text == "See Section 5 for volume tiers."
        assert result.relationships.definitions[0].term == "QCC"
        assert result.relationships.definitions[0].text == "Qualified Contingent Cross"
        assert result.relationships.definitions[1].term == "ADV"

    def test_relationships_default_none(self):
        result = FeeScheduleResult.model_validate(RESULT_1)
        assert result.relationships is None

    def test_relationships_empty_lists(self):
        data = {**RESULT_1, "relationships": {"footnotes": [], "definitions": []}}
        result = FeeScheduleResult.model_validate(data)
        assert result.relationships is not None
        assert result.relationships.footnotes == []
        assert result.relationships.definitions == []


class TestFootnoteModel:
    """Tests for the Footnote Pydantic model."""

    def test_from_dict(self):
        fn = Footnote.model_validate({"text": "See note 1"})
        assert fn.text == "See note 1"

    def test_frozen(self):
        fn = Footnote.model_validate({"text": "Test"})
        with pytest.raises(Exception):
            fn.text = "MODIFIED"  # type: ignore[misc]


class TestDefinitionModel:
    """Tests for the Definition Pydantic model."""

    def test_from_dict(self):
        defn = Definition.model_validate({"term": "ADV", "text": "Average Daily Volume"})
        assert defn.term == "ADV"
        assert defn.text == "Average Daily Volume"

    def test_frozen(self):
        defn = Definition.model_validate({"term": "ADV", "text": "Average Daily Volume"})
        with pytest.raises(Exception):
            defn.term = "MODIFIED"  # type: ignore[misc]


class TestRelationshipsModel:
    """Tests for the Relationships Pydantic model."""

    def test_from_dict(self):
        rel = Relationships.model_validate({
            "footnotes": [{"text": "Note 1"}, {"text": "Note 2"}],
            "definitions": [{"term": "QCC", "text": "Qualified Contingent Cross"}],
        })
        assert len(rel.footnotes) == 2
        assert len(rel.definitions) == 1
        assert isinstance(rel.footnotes[0], Footnote)
        assert isinstance(rel.definitions[0], Definition)

    def test_defaults_to_empty_lists(self):
        rel = Relationships.model_validate({})
        assert rel.footnotes == []
        assert rel.definitions == []

    def test_frozen(self):
        rel = Relationships.model_validate({"footnotes": [], "definitions": []})
        with pytest.raises(Exception):
            rel.footnotes = []  # 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_meta_envelope(self):
        """Parse the real API response shape with nested meta."""
        page = PaginatedResponse[FeeScheduleResult].model_validate({
            "data": [RESULT_1],
            "meta": {
                "record_count": 1,
                "page_size": 20,
                "total_pages": 1,
                "current_page": 0,
                "query_time_ms": 42,
            },
        })
        assert page.total_records == 1
        assert page.page_size == 20
        assert page.total_pages == 1
        assert page.current_page == 0
        assert page.query_time_ms == 42
        assert len(page.data) == 1
        assert isinstance(page.data[0], FeeScheduleResult)
        assert page.data[0].exchange_name == "CBOE"

    def test_from_flat_dict(self):
        """Flat fields (without meta) still work for backwards compatibility."""
        page = PaginatedResponse[FeeScheduleResult].model_validate({
            "data": [RESULT_1],
            "total_records": 1,
            "page_size": 20,
            "total_pages": 1,
            "current_page": 0,
            "query_time_ms": 10,
        })
        assert page.total_records == 1
        assert page.page_size == 20
        assert len(page.data) == 1
        assert isinstance(page.data[0], FeeScheduleResult)

    def test_query_time_ms_null(self):
        """query_time_ms=null from API results in query_time_ms=None."""
        page = PaginatedResponse[FeeScheduleResult].model_validate({
            "data": [],
            "meta": {
                "record_count": 0,
                "page_size": 20,
                "total_pages": 0,
                "current_page": 0,
                "query_time_ms": None,
            },
        })
        assert page.query_time_ms is None

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