"""
:mod:`tests.unit.file.pytest_file_contracts_binary` module.

Binary/archive/stub contract suites for unit tests of :mod:`etlplus.file`.
"""

from __future__ import annotations

from pathlib import Path
from typing import cast

import pytest

from etlplus.file import FileFormat
from etlplus.file.stub import StubFileHandlerABC
from etlplus.utils._types import JSONData
from etlplus.utils._types import JSONDict

from .pytest_file_contract_mixins import PathMixin
from .pytest_file_contract_utils import Operation
from .pytest_file_contract_utils import assert_stub_module_operation_raises
from .pytest_file_contract_utils import make_payload
from .pytest_file_types import OptionalModuleInstaller

# SECTION: PRAGMAS ========================================================== #

# pylint: disable=import-outside-toplevel,protected-access,unused-argument

# SECTION: EXPORTS ========================================================== #


__all__ = [
    'ArchiveWrapperCoreDispatchModuleContract',
    'BinaryCodecModuleContract',
    'BinaryDependencyModuleContract',
    'BinaryKeyedPayloadModuleContract',
    'StubModuleContract',
]


# SECTION: CLASSES ========================================================== #


class BinaryCodecStub:
    """
    Generic codec stub for binary serialization module tests.

    Supports configurable reader/writer method names to cover modules like
    :mod:`msgpack` and :mod:`cbor2` with one reusable implementation.
    """

    def __init__(
        self,
        *,
        reader_method_name: str,
        writer_method_name: str,
        loaded_result: object,
        emitted_bytes: bytes,
    ) -> None:
        self.reader_method_name = reader_method_name
        self.writer_method_name = writer_method_name
        self.loaded_result = loaded_result
        self.emitted_bytes = emitted_bytes
        self.reader_payloads: list[bytes] = []
        self.reader_kwargs: list[dict[str, object]] = []
        self.writer_payloads: list[object] = []
        self.writer_kwargs: list[dict[str, object]] = []

    def _reader(
        self,
        payload: bytes,
        **kwargs: object,
    ) -> object:
        self.reader_payloads.append(payload)
        self.reader_kwargs.append(dict(kwargs))
        return self.loaded_result

    def _writer(
        self,
        payload: object,
        **kwargs: object,
    ) -> bytes:
        self.writer_payloads.append(payload)
        self.writer_kwargs.append(dict(kwargs))
        return self.emitted_bytes

    def __getattr__(
        self,
        name: str,
    ) -> object:
        if name == self.reader_method_name:
            return self._reader
        if name == self.writer_method_name:
            return self._writer
        raise AttributeError(name)


class CoreDispatchFileStub:
    """
    Minimal stand-in for :class:`etlplus.file._core.File` in archive tests.
    """

    def __init__(
        self,
        path: Path,
        fmt: FileFormat,
    ) -> None:
        self.path = Path(path)
        self.fmt = fmt

    def read(self) -> dict[str, str]:
        """Return deterministic payload for archive-wrapper read tests."""
        return {'fmt': self.fmt.value, 'name': self.path.name}

    def write(
        self,
        data: object,
    ) -> int:
        """Persist deterministic content so wrapper tests can assert bytes."""
        _ = data
        self.path.write_text('payload', encoding='utf-8')
        return 1


# SECTION: CLASSES (CONTRACTS) ============================================== #


class ArchiveWrapperCoreDispatchModuleContract(PathMixin):
    """Reusable contract suite for archive wrappers using core dispatch."""

    write_payload: JSONData = make_payload('list')
    expected_written_count: int = 1
    missing_inner_error_pattern: str = 'Cannot infer file format'

    def archive_path(
        self,
        tmp_path: Path,
        *,
        stem: str,
        suffix: str | None = None,
    ) -> Path:
        """Build deterministic archive paths for ad hoc test cases."""
        extension = self.format_name if suffix is None else suffix
        return tmp_path / f'{stem}.{extension}'

    def valid_archive_path(
        self,
        tmp_path: Path,
    ) -> Path:
        """Build the canonical archive path for core-dispatch tests."""
        return self.archive_path(tmp_path, stem='payload.json')

    def missing_inner_format_path(
        self,
        tmp_path: Path,
    ) -> Path:
        """Build an archive path with no inferable inner file format."""
        return self.archive_path(tmp_path, stem='payload')

    def expected_read_result(
        self,
    ) -> JSONData:
        """Build the expected core-dispatch payload for archive reads."""
        return {'fmt': 'json', 'name': 'payload.json'}

    def seed_archive_payload(
        self,
        path: Path,
    ) -> None:
        """Write a wrapped payload used by read tests."""
        raise NotImplementedError

    def assert_archive_payload(
        self,
        path: Path,
    ) -> None:
        """Assert wrapped payload bytes/content produced by writes."""
        raise NotImplementedError

    def install_core_file_stub(
        self,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Patch core file dispatch for deterministic archive tests."""
        monkeypatch.setattr(
            'etlplus.file._core.File',
            CoreDispatchFileStub,
        )

    def test_read_uses_core_dispatch(
        self,
        tmp_path: Path,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Test read delegating payload parsing through core dispatch."""
        self.install_core_file_stub(monkeypatch)
        path = self.valid_archive_path(tmp_path)
        self.seed_archive_payload(path)

        result = self.module_handler.read(path)

        assert result == self.expected_read_result()

    def test_write_creates_wrapped_payload(
        self,
        tmp_path: Path,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Test write persisting wrapped payload through core dispatch."""
        self.install_core_file_stub(monkeypatch)
        path = self.valid_archive_path(tmp_path)

        written = self.module_handler.write(path, self.write_payload)

        assert written == self.expected_written_count
        self.assert_archive_payload(path)

    def test_write_requires_inner_format(
        self,
        tmp_path: Path,
    ) -> None:
        """Test writes requiring a resolvable inner file format."""
        path = self.missing_inner_format_path(tmp_path)

        with pytest.raises(ValueError, match=self.missing_inner_error_pattern):
            self.module_handler.write(path, self.write_payload)


class BinaryCodecModuleContract(PathMixin):
    """Reusable contract suite for binary codec wrapper modules."""

    dependency_name: str
    reader_method_name: str
    writer_method_name: str
    reader_kwargs: dict[str, object]
    writer_kwargs: dict[str, object]
    loaded_result: JSONData
    emitted_bytes: bytes
    list_payload: JSONData = make_payload('list')
    dict_payload: JSONData = make_payload('dict')
    expected_list_dump: object = make_payload('list')
    expected_dict_dump: object = make_payload('dict')

    def _make_codec_stub(
        self,
        *,
        loaded_result: object,
    ) -> BinaryCodecStub:
        """Create a codec stub configured for this binary format module."""
        return BinaryCodecStub(
            reader_method_name=self.reader_method_name,
            writer_method_name=self.writer_method_name,
            loaded_result=loaded_result,
            emitted_bytes=self.emitted_bytes,
        )

    def test_read_uses_dependency_codec(
        self,
        tmp_path: Path,
        optional_module_stub: OptionalModuleInstaller,
    ) -> None:
        """Test read delegating bytes decoding to the codec dependency."""
        codec = self._make_codec_stub(loaded_result=self.loaded_result)
        optional_module_stub({self.dependency_name: codec})
        path = self.format_path(tmp_path)
        path.write_bytes(b'payload')

        result = self.module_handler.read(path)

        assert result == self.loaded_result
        assert codec.reader_payloads == [b'payload']
        assert codec.reader_kwargs == [self.reader_kwargs]

    @pytest.mark.parametrize(
        ('payload_attr', 'expected_attr'),
        [
            ('list_payload', 'expected_list_dump'),
            ('dict_payload', 'expected_dict_dump'),
        ],
        ids=['list', 'dict'],
    )
    def test_write_serializes_payload(
        self,
        tmp_path: Path,
        optional_module_stub: OptionalModuleInstaller,
        payload_attr: str,
        expected_attr: str,
    ) -> None:
        """Test write delegating payload encoding to the codec dependency."""
        codec = self._make_codec_stub(loaded_result=self.loaded_result)
        optional_module_stub({self.dependency_name: codec})
        path = self.format_path(tmp_path)
        payload = cast(JSONData, getattr(self, payload_attr))
        expected_dump = getattr(self, expected_attr)

        written = self.module_handler.write(path, payload)

        assert written == 1
        assert codec.writer_payloads == [expected_dump]
        assert codec.writer_kwargs == [self.writer_kwargs]
        assert path.read_bytes() == self.emitted_bytes


class BinaryDependencyModuleContract(PathMixin):
    """Reusable contract suite for binary modules backed by one dependency."""

    dependency_name: str
    expected_read_result: JSONData
    write_payload: JSONData
    read_payload_bytes: bytes = b'payload'
    expected_written_count: int = 1

    def make_dependency_stub(self) -> object:
        """Build dependency stub used by read/write tests."""
        raise NotImplementedError

    def assert_dependency_after_read(
        self,
        dependency_stub: object,
        path: Path,  # noqa: ARG002
    ) -> None:
        """Assert dependency interactions for read tests."""

    def assert_dependency_after_write(
        self,
        dependency_stub: object,
        path: Path,  # noqa: ARG002
    ) -> None:
        """Assert dependency interactions for write tests."""

    def test_read_uses_dependency(
        self,
        tmp_path: Path,
        optional_module_stub: OptionalModuleInstaller,
    ) -> None:
        """Test read delegating to the configured dependency."""
        dependency = self.make_dependency_stub()
        optional_module_stub({self.dependency_name: dependency})
        path = self.format_path(tmp_path)
        path.write_bytes(self.read_payload_bytes)

        result = self.module_handler.read(path)

        assert result == self.expected_read_result
        self.assert_dependency_after_read(dependency, path)

    def test_write_uses_dependency(
        self,
        tmp_path: Path,
        optional_module_stub: OptionalModuleInstaller,
    ) -> None:
        """Test write delegating to the configured dependency."""
        dependency = self.make_dependency_stub()
        optional_module_stub({self.dependency_name: dependency})
        path = self.format_path(tmp_path)

        written = self.module_handler.write(path, self.write_payload)

        assert written == self.expected_written_count
        self.assert_dependency_after_write(dependency, path)


class BinaryKeyedPayloadModuleContract(PathMixin):
    """Reusable contract suite for keyed binary payload wrapper modules."""

    payload_key: str
    sample_payload_value: str
    expected_bytes: bytes
    invalid_payload: JSONData

    @pytest.fixture
    def sample_payload(self) -> JSONDict:
        """Create a representative keyed payload dictionary."""
        return {self.payload_key: self.sample_payload_value}

    def test_read_write_roundtrip(
        self,
        tmp_path: Path,
        sample_payload: JSONDict,
    ) -> None:
        """Test write/read round trip, preserving payload bytes."""
        path = self.format_path(tmp_path)

        written = self.module_handler.write(path, sample_payload)

        assert written == 1
        assert path.read_bytes() == self.expected_bytes
        assert self.module_handler.read(path) == sample_payload

    def test_write_rejects_missing_required_key(
        self,
        tmp_path: Path,
    ) -> None:
        """Test writes requiring the expected payload key."""
        path = self.format_path(tmp_path)

        with pytest.raises(TypeError, match=self.payload_key):
            self.module_handler.write(path, self.invalid_payload)


class StubModuleContract(PathMixin):
    """Reusable contract suite for placeholder/stub format modules."""

    handler_cls: type[StubFileHandlerABC]

    def test_handler_inherits_stub_abc(self) -> None:
        """Test handler metadata and inheritance contract."""
        assert issubclass(self.handler_cls, StubFileHandlerABC)
        assert self.handler_cls.format.value == self.format_name

    @pytest.mark.parametrize(
        ('operation', 'write_payload'),
        [
            ('read', None),
            ('write', None),
            ('write', []),
        ],
    )
    def test_module_operations_raise_not_implemented(
        self,
        tmp_path: Path,
        operation: Operation,
        write_payload: JSONData | None,
    ) -> None:
        """
        Test placeholder handler operations raising
        :class:`NotImplementedError`.
        """
        assert_stub_module_operation_raises(
            self.module,
            format_name=self.format_name,
            operation=operation,
            path=self.format_path(tmp_path),
            write_payload=write_payload,
        )
