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

Unit tests for :mod:`etlplus.file.avro`.
"""

from __future__ import annotations

from collections.abc import Callable
from pathlib import Path

import pytest

from etlplus.file import avro as mod
from etlplus.file.enums import FileFormat

from .pytest_file_contract_utils import patch_dependency_resolver_unreachable
from .pytest_file_contracts import BinaryDependencyModuleContract
from .pytest_file_types import OptionalModuleInstaller

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

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

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


_TYPE_ERROR_PATTERN = 'AVRO payloads must contain'


class _FastAvroStub:
    """Stub for the :mod:`fastavro` module."""

    def __init__(self) -> None:
        self.parsed_schema: dict[str, object] | None = None
        self.writes: list[dict[str, object]] = []
        self.records: list[dict[str, object]] = [{'id': 1}, {'id': 2}]

    def parse_schema(
        self,
        schema: dict[str, object],
    ) -> dict[str, object]:
        """Simulate parsing an Avro schema."""
        self.parsed_schema = schema
        return schema

    def reader(
        self,
        handle: object,
    ) -> list[dict[str, object]]:  # noqa: ARG002
        """Simulate reading records from a file handle."""
        return list(self.records)

    def writer(
        self,
        handle: object,  # noqa: ARG002
        schema: dict[str, object],
        records: list[dict[str, object]],
    ) -> None:
        """Simulate writing records to a file handle."""
        self.writes.append({'schema': schema, 'records': list(records)})


# SECTION: TESTS ============================================================ #


class TestAvroHandlerClass:
    """Unit tests for :class:`etlplus.file.avro.AvroFile`."""

    def test_dumps_and_loads_bytes(
        self,
        optional_module_stub: OptionalModuleInstaller,
    ) -> None:
        """Test that binary helper methods delegating to :mod:`fastavro`."""
        stub = _FastAvroStub()
        optional_module_stub({'fastavro': stub})
        handler = mod.AvroFile()
        records = [{'id': 1, 'name': 'Ada'}]

        payload = handler.dumps_bytes(records)
        result = handler.loads_bytes(payload)

        assert isinstance(payload, bytes)
        assert stub.parsed_schema is not None
        assert stub.writes
        assert result == stub.records

    @pytest.mark.parametrize(
        ('operation', 'expected'),
        [
            (lambda h, _path: h.dumps_bytes([]), b''),
            (lambda h, path: h.write(path, []), 0),
        ],
        ids=['dumps_bytes_empty', 'write_empty'],
    )
    def test_empty_payload_short_circuits(
        self,
        tmp_path: Path,
        monkeypatch: pytest.MonkeyPatch,
        operation: Callable[[mod.AvroFile, Path], bytes | int],
        expected: bytes | int,
    ) -> None:
        """
        Test that empty payload operations short-circuit without loading
        optional dependencies.
        """
        handler = mod.AvroFile()
        patch_dependency_resolver_unreachable(monkeypatch, mod)
        result = operation(handler, tmp_path / 'sample.avro')
        assert result == expected

    def test_format_constant(self) -> None:
        """Test that :class:`AvroFile` exposes the expected format enum."""
        assert mod.AvroFile.format is FileFormat.AVRO


class TestAvroHelpers:
    """Unit tests for :mod:`etlplus.file.avro` helpers."""

    @pytest.mark.parametrize(
        ('operation', 'payload'),
        [
            (mod._infer_value_type, {'bad': 'value'}),
            (mod._infer_schema, [{'bad': {'nested': True}}]),
            (mod._infer_schema, [{'bad': object()}]),
        ],
        ids=[
            'infer_value_type_complex',
            'infer_schema_nested',
            'infer_schema_non_primitive_scalar',
        ],
    )
    def test_infer_helpers_reject_invalid_payloads(
        self,
        operation: Callable[[object], object],
        payload: object,
    ) -> None:
        """Test that infer helpers raising for unsupported complex payloads."""
        with pytest.raises(TypeError, match=_TYPE_ERROR_PATTERN):
            operation(payload)

    def test_infer_schema_builds_fields(self) -> None:
        """Test that :func:`_infer_schema` builds expected fields."""
        records: list[dict[str, object]] = [
            {'b': 'text', 'a': 1},
            {'b': None},
        ]

        schema = mod._infer_schema(records)

        assert schema['type'] == 'record'
        assert schema['name'] == 'etlplus_record'
        assert schema['fields'] == [
            {'name': 'a', 'type': ['null', 'long']},
            {'name': 'b', 'type': ['null', 'string']},
        ]

    @pytest.mark.parametrize(
        ('value', 'expected'),
        [
            (None, 'null'),
            (True, 'boolean'),
            (1, 'long'),
            (1.25, 'double'),
            ('text', 'string'),
            (b'bytes', 'bytes'),
            (bytearray(b'data'), 'bytes'),
        ],
        ids=[
            'null',
            'bool',
            'int',
            'float',
            'str',
            'bytes',
            'bytearray',
        ],
    )
    def test_infer_value_type_supported(
        self,
        value: object,
        expected: str,
    ) -> None:
        """
        Test that :func:`_infer_value_type` returns expected types for
        supported values.
        """
        assert mod._infer_value_type(value) == expected

    def test_merge_types_orders_null_first(self) -> None:
        """
        Test that :func:`_merge_types` orders 'null' first when present.
        """
        types = ['string', 'null', 'string', 'long']

        merged = mod._merge_types(types)

        assert merged == ['null', 'long', 'string']


class TestAvroIo(BinaryDependencyModuleContract):
    """Unit tests for AVRO handler read/write behavior."""

    module = mod
    format_name = 'avro'
    dependency_name = 'fastavro'
    expected_read_result = [{'id': 1}, {'id': 2}]
    write_payload = [{'id': 1, 'name': 'Ada'}]

    def assert_dependency_after_write(
        self,
        dependency_stub: object,
        _path: Path,
    ) -> None:
        """Assert fastavro write behavior."""
        stub = dependency_stub
        assert isinstance(stub, _FastAvroStub)
        assert stub.parsed_schema is not None
        assert stub.writes
        assert stub.writes[0]['records'] == self.write_payload

    def assert_dependency_after_read(
        self,
        dependency_stub: object,
        _path: Path,
    ) -> None:
        """Assert fastavro read behavior."""
        assert isinstance(dependency_stub, _FastAvroStub)

    def make_dependency_stub(self) -> _FastAvroStub:
        """Build a fastavro dependency stub."""
        return _FastAvroStub()
