"""
:mod:`tests.integration.conftest` module.

Configures pytest-based integration tests and provides shared fixtures.

Notes
-----
- Fixtures are designed for reuse and DRY test setup.
"""

from __future__ import annotations

import importlib
import pathlib
from collections.abc import Callable
from typing import Any
from typing import Protocol

import pytest

from etlplus.api import ApiConfig
from etlplus.api import ApiProfileConfig
from etlplus.api import EndpointConfig
from etlplus.api import PaginationConfig
from etlplus.api import RateLimitConfig
from etlplus.api import RateLimiter
from etlplus.config import ConnectorApi
from etlplus.config import ConnectorFile
from etlplus.config import ExtractRef
from etlplus.config import JobConfig
from etlplus.config import LoadRef
from etlplus.config import PipelineConfig

# SECTION: HELPERS ========================================================== #


# Directory-level marker for integration tests.
pytestmark = pytest.mark.integration


# Protocol describing the fake endpoint client shape for type checking.
class FakeEndpointClientProtocol(Protocol):
    """
    Protocol for fake endpoint clients used in integration tests.

    Attributes
    ----------
    seen : dict[str, Any]
        Dictionary capturing values observed during pagination.
    """

    seen: dict[str, Any]


# SECTION: FIXTURES ========================================================= #


@pytest.fixture
def capture_load_to_api(
    monkeypatch: pytest.MonkeyPatch,
) -> dict[str, Any]:
    """
    Capture API load calls by patching the high-level ``load`` wrapper.

    We monkeypatch ``etlplus.load.load`` because the runner invokes
    ``load(data, 'api', url, method=...)`` rather than calling
    ``load_to_api`` directly. The stub detects API loads and records
    request details for assertions.

    Parameters
    ----------
    monkeypatch : pytest.MonkeyPatch
        Pytest monkeypatch fixture for patching functions.

    Returns
    -------
    dict[str, Any]
        Mutable dictionary populated after a run where API load happens.
    """
    # Ensure module, not attr.
    _load_mod = importlib.import_module('etlplus.load')
    _run_mod = importlib.import_module('etlplus.run')

    seen: dict[str, Any] = {}
    real_load = _load_mod.load

    def _fake_load(
        data: Any,
        target_type: str,
        target: str,
        **kwargs: Any,
    ) -> dict[str, Any]:
        if target_type == 'api':
            seen['url'] = target
            seen['method'] = kwargs.get('method') or 'post'
            seen['headers'] = kwargs.get('headers')
            seen['timeout'] = kwargs.get('timeout')
            seen['session'] = kwargs.get('session')

            # Return a minimal success envelope similar to real load_to_api.
            return {
                'status': 'ok',
                'url': target,
                'count': len(data) if isinstance(data, list) else 0,
            }

        return real_load(data, target_type, target, **kwargs)

    monkeypatch.setattr(_load_mod, 'load', _fake_load)
    monkeypatch.setattr(_run_mod, 'load', _fake_load)

    return seen


@pytest.fixture
def fake_endpoint_client() -> tuple[
    type[FakeEndpointClientProtocol],
    list[FakeEndpointClientProtocol],
]:  # noqa: ANN201
    """
    Provide a Fake EndpointClient class and capture list.

    The returned class records 'pagination' and 'sleep_seconds' seen by
    its paginate() method. The second element of the tuple is the list of
    created instances for assertions.

    Returns
    -------
    tuple[type[FakeEndpointClientProtocol], list[FakeEndpointClientProtocol]]
        A tuple where the first element is the FakeClient class and the
        second element is the list of created instances.
    """
    created: list[FakeEndpointClientProtocol] = []

    class FakeClient:
        """
        Fake :class:`EndpointClient` capturing :meth:`paginate` arguments.
        """

        # pylint: disable=unused-argument

        seen: dict[str, Any]

        def __init__(
            self,
            base_url: str,
            endpoints: dict[str, str],
            *args,
            **kwargs,
        ):
            self.base_url = base_url
            self.endpoints = endpoints
            self.seen = {}
            created.append(self)

        def paginate(
            self,
            *args,
            pagination: Any | None = None,
            sleep_seconds: float = 0.0,
            **kwargs,
        ) -> Any:
            """
            Capture pagination config and sleep seconds.
            """
            self.seen['pagination'] = pagination
            self.seen['sleep_seconds'] = sleep_seconds
            return [{'ok': True}]

    return FakeClient, created


@pytest.fixture
def pipeline_cfg_factory(
    tmp_path: pathlib.Path,
) -> Callable[..., PipelineConfig]:
    """
    Factory to build a minimal PipelineConfig for runner tests.

    Accepts optional pagination and rate limit defaults at the API profile
    level. Creates a single job named 'job' with a source that references
    the API endpoint 'items' and a file target.

    Parameters
    ----------
    tmp_path : pathlib.Path
        The pytest temporary path fixture.

    Returns
    -------
    Callable[..., PipelineConfig]
        Factory function to create :class:`PipelineConfig` instances.
    """

    def _make(
        *,
        pagination_defaults: PaginationConfig | None = None,
        rate_limit_defaults: RateLimitConfig | None = None,
    ) -> PipelineConfig:
        prof = ApiProfileConfig(
            base_url='https://api.example.com',
            headers={},
            base_path='/v1',
            auth={},
            rate_limit_defaults=rate_limit_defaults,
            pagination_defaults=pagination_defaults,
        )
        api = ApiConfig(
            base_url=prof.base_url,
            headers=prof.headers,
            profiles={'default': prof},
            endpoints={'items': EndpointConfig(path='/items')},
        )
        src = ConnectorApi(name='s', type='api', api='svc', endpoint='items')
        out_path = tmp_path / 'out.json'
        tgt = ConnectorFile(
            name='t',
            type='file',
            format='json',
            path=str(out_path),
        )
        return PipelineConfig(
            apis={'svc': api},
            sources=[src],
            targets=[tgt],
            jobs=[
                JobConfig(
                    name='job',
                    extract=ExtractRef(source='s'),
                    load=LoadRef(target='t'),
                ),
            ],
        )

    return _make


@pytest.fixture
def run_patched(
    monkeypatch: pytest.MonkeyPatch,
) -> Callable[..., dict[str, Any]]:
    """
    Return a helper to run the pipeline with patched runner dependencies.

    Parameters
    ----------
    monkeypatch : pytest.MonkeyPatch
        The pytest monkeypatch fixture.

    Returns
    -------
    Callable[..., dict[str, Any]]
        Factory function to run the pipeline with patched dependencies.

    Example
    -------
    result = run_patched(cfg, FakeClient, sleep_seconds=1.23)
    """
    run_mod = importlib.import_module('etlplus.run')

    def _run(
        cfg: PipelineConfig,
        endpoint_client_cls: type,
        *,
        sleep_seconds: float | None = None,
    ) -> dict[str, Any]:
        # pylint: disable=unused-argument

        # Patch config loader and EndpointClient.
        monkeypatch.setattr(
            run_mod,
            'load_pipeline_config',
            lambda *_a, **_k: cfg,
        )
        monkeypatch.setattr(run_mod, 'EndpointClient', endpoint_client_cls)

        # Optionally force the resolved rate-limit delay to a constant.
        if sleep_seconds is not None:

            def _fixed_resolve(
                cls: type[RateLimiter],
                *,
                rate_limit: Any | None = None,
                overrides: Any | None = None,
            ) -> float:
                # ignore inputs and return deterministic delay for assertions
                return sleep_seconds

            monkeypatch.setattr(
                RateLimiter,
                'resolve_sleep_seconds',
                classmethod(_fixed_resolve),
            )

        # Avoid real IO in load().
        def _fake_load(
            data: Any,
            *args,
            **kwargs,
        ) -> dict[str, Any]:
            n = len(data) if isinstance(data, list) else 0
            return {'status': 'ok', 'count': n}

        monkeypatch.setattr(run_mod, 'load', _fake_load)

        return run_mod.run('job')

    return _run
