"""Test the filters.py file."""

import numpy as np
import pandas as pd
import pytest
from openbb_core.app.model.abstract.error import OpenBBError
from openbb_core.app.static.utils.filters import filter_inputs
from openbb_core.provider.abstract.data import Data


def test_filter_inputs_not_df():
    """Test filter_inputs."""
    kwargs = {"num": 1}
    kwargs = filter_inputs(False, None, **kwargs)

    assert kwargs["num"] == 1


def test_filter_inputs_df():
    """Test filter_inputs."""
    df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
    kwargs = {"data": df}
    kwargs = filter_inputs(True, None, **kwargs)

    assert isinstance(kwargs["data"], list)


# Example instances of each supported type for testing
example_dict = {"a": 1, "b": 2}
example_list = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
example_series = pd.Series([1, 2, 3])
example_dataframe = pd.DataFrame({"col1": [1, 2], "col2": [3, 4]})
example_ndarray = np.array([[1, 2], [3, 4]])
example_data_list_series = [pd.Series([4, 5, 6])]
example_data_list_df = [pd.DataFrame({"col3": [5, 6], "col4": [7, 8]})]

# Create a list of scenarios to test
test_data = [
    example_dict,
    example_list,
    example_series,
    example_dataframe,
    example_ndarray,
    example_data_list_series,
    example_data_list_df,
]


@pytest.mark.parametrize("input_data", test_data)
def test_filter_inputs(
    input_data,
):
    """Test the filter_inputs function."""
    result = filter_inputs(data=input_data, data_processing=True)

    # Assert that the result is a dictionary
    assert isinstance(result, dict), "filter_inputs should return a dictionary"

    # Assert that the 'data' key is present in the result
    assert "data" in result, "Resulting dictionary should have a 'data' key"

    # Assert that the type of 'data' in the result is the expected type
    if isinstance(result["data"], list):
        assert isinstance(
            result["data"][0], Data
        ), f"The 'data' key should be a list of {Data.__name__}"
    else:
        assert isinstance(
            result["data"], Data
        ), f"The 'data' key should be of type {Data.__name__}"


# --- Choices validation tests ---
# These tests cover the fix for a silent data corruption bug:
#
# EXACT BUG SCENARIO (obb.economy.balance_of_payments):
#   - ECB's `frequency` default is "monthly"; OECD only supports "annual"/"quarterly".
#   - `openbb-build` merges provider schemas into a single ExtraParams dataclass whose
#     `frequency` field has type Union[Literal['monthly','quarterly'], Literal['annual',
#     'quarterly']] and default="monthly" (taken from ECB, first alphabetically).
#   - "mont" was correctly rejected: it satisfies neither Literal, so ExtraParams
#     instantiation raises a ValidationError before any provider logic runs.
#   - "monthly" silently passed: it satisfies ECB's Literal, so ExtraParams accepted
#     it. `filter_extra_params` then saw frequency="monthly" == merged default="monthly"
#     and DROPPED the parameter entirely. OECD's transform_query received no `frequency`
#     kwarg and fell back to its own pydantic default "quarterly", returning quarterly
#     data with zero error or warning.
#
# The fix has two components:
#   1. `_create_field` in `provider_interface.py` now auto-derives per-provider choices
#      from Literal type annotations. A provider field declared as
#      `frequency: Literal["annual", "quarterly"]` causes openbb-build to emit
#      `"frequency": {"oecd": {"choices": ["annual", "quarterly"]}}` into the info dict
#      of the generated package file — no manual `__json_schema_extra__` entry needed.
#   2. `filter_inputs` validates the raw user kwargs dict against those choices BEFORE
#      the merged ExtraParams dataclass is built, so the explicitly-passed "monthly"
#      is caught at the boundary regardless of what the merged default is.

# Info dict WITH choices — mirrors the post-fix `economy.py` generated by openbb-build.
# Choices are now auto-derived from the provider's Literal annotation:
#   `frequency: Literal["annual", "quarterly"]`   →   choices=["annual", "quarterly"]
# No manual `__json_schema_extra__` entry is required.
_BOP_INFO_WITH_CHOICES = {
    "frequency": {
        "oecd": {"multiple_items_allowed": False, "choices": ["annual", "quarterly"]},
    }
}

# Info dict WITHOUT choices — represents the pre-fix state where Literal annotations
# were NOT reflected in the info dict, so filter_inputs had nothing to validate against.
_BOP_INFO_WITHOUT_CHOICES = {
    "frequency": {
        "oecd": {"multiple_items_allowed": False},
    }
}

# Shorter alias used by the general-purpose tests below.
_BOP_INFO = _BOP_INFO_WITH_CHOICES


def _bop_kwargs(provider: str, **extra) -> dict:
    """Return a filter_inputs kwargs structure for the BOP endpoint."""
    return {
        "provider_choices": {"provider": provider},
        "standard_params": {},
        "extra_params": {"frequency": "quarterly", **extra},
    }


def test_filter_inputs_choices_valid_value_passes():
    """A value that is in the allowed choices must be accepted without error."""
    kwargs = _bop_kwargs("oecd", frequency="annual")
    result = filter_inputs(info=_BOP_INFO, **kwargs)
    assert result["extra_params"]["frequency"] == "annual"


def test_filter_inputs_choices_default_value_passes():
    """The default value ('quarterly') must also be accepted."""
    kwargs = _bop_kwargs("oecd")  # frequency defaults to 'quarterly'
    result = filter_inputs(info=_BOP_INFO, **kwargs)
    assert result["extra_params"]["frequency"] == "quarterly"


def test_filter_inputs_choices_invalid_value_raises():
    """
    Regression test: 'monthly' must be rejected for the 'oecd' provider.

    Before the fix, `filter_extra_params` would silently drop `frequency="monthly"`
    because it equalled the merged ExtraParams default (taken from ECB). The OECD
    fetcher then used its own pydantic default 'quarterly', returning quarterly data
    with no error or warning.

    After the fix, `filter_inputs` validates the value against the provider's allowed
    choices and raises an `OpenBBError` with a descriptive message.
    """
    kwargs = _bop_kwargs("oecd", frequency="monthly")
    with pytest.raises(OpenBBError, match="Invalid value 'monthly' for 'frequency'"):
        filter_inputs(info=_BOP_INFO, **kwargs)


def test_filter_inputs_choices_not_enforced_for_other_provider():
    """
    Choices defined for 'oecd' must NOT apply when a different provider is selected.
    ECB legitimately accepts 'monthly', so it must pass without error.
    """
    kwargs = _bop_kwargs("ecb", frequency="monthly")
    result = filter_inputs(info=_BOP_INFO, **kwargs)
    assert result["extra_params"]["frequency"] == "monthly"


def test_filter_inputs_choices_no_info_no_error():
    """When no `info` dict is supplied, no choices validation occurs."""
    kwargs = {
        "provider_choices": {"provider": "oecd"},
        "extra_params": {"frequency": "monthly"},
    }
    # Must not raise even though 'monthly' would be invalid for oecd with choices
    result = filter_inputs(**kwargs)
    assert result["extra_params"]["frequency"] == "monthly"


# --- Negative tests: exact bug reproduction ---
# These two tests mirror the discriminating condition that first exposed the bug:
#   "mont"   → rejected  (caught by ExtraParams Literal validation — was already working)
#   "monthly" → accepted  (equalled the merged default → silently dropped → wrong data)


def test_filter_inputs_pre_fix_monthly_passed_without_choices():
    """
    Pre-fix negative: without choices in the info dict, 'monthly' passes through
    filter_inputs unchallenged — exactly as it did before the fix.

    Before the fix, Literal annotations on provider fields were not reflected in the
    info dict written by openbb-build, so filter_inputs had no choices to validate
    against. Once 'monthly' exits filter_inputs, filter_extra_params sees
    v == merged_default ("monthly") and silently drops the parameter, so the OECD
    model never validates it.
    """
    kwargs = {
        "provider_choices": {"provider": "oecd"},
        "standard_params": {},
        "extra_params": {"frequency": "monthly"},
    }
    # No choices → no rejection → 'monthly' passes through (pre-fix behaviour)
    result = filter_inputs(info=_BOP_INFO_WITHOUT_CHOICES, **kwargs)
    assert result["extra_params"]["frequency"] == "monthly"


def test_filter_inputs_post_fix_monthly_rejected_with_choices():
    """
    Post-fix negative: with choices in the info dict, the same 'monthly' value
    that used to silently corrupt the result is now rejected with a clear error.

    Choices are now auto-derived by openbb-build from the provider's Literal annotation
    (`frequency: Literal["annual", "quarterly"]`), so no manual __json_schema_extra__
    entry is required. Together with the test above this pair proves:
      - 'mont'    → already failed (Literal Union rejects it at ExtraParams build time)
      - 'monthly' → used to pass  (satisfied ECB's Literal, equalled merged default)
      - 'monthly' → now fails     (filter_inputs catches it via choices before ExtraParams)
    """
    kwargs = {
        "provider_choices": {"provider": "oecd"},
        "standard_params": {},
        "extra_params": {"frequency": "monthly"},
    }
    with pytest.raises(OpenBBError, match="Invalid value 'monthly' for 'frequency'"):
        filter_inputs(info=_BOP_INFO_WITH_CHOICES, **kwargs)
