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

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

from __future__ import annotations

import math
import numbers
import sqlite3
import zipfile
from io import BytesIO
from pathlib import Path
from types import SimpleNamespace
from typing import Any
from typing import cast

import pytest

from etlplus.file import File
from etlplus.file import FileFormat
from etlplus.file import core as core_mod
from etlplus.file import csv as csv_file
from etlplus.file import json as json_file
from etlplus.file import xml as xml_file
from etlplus.file.base import WriteOptions
from etlplus.storage import S3StorageBackend
from etlplus.storage import StorageScheme
from etlplus.storage import get_backend
from etlplus.storage import http as http_storage_mod
from etlplus.utils.types import JSONData

from ...pytest_file_common import Operation
from ...pytest_file_common import skip_on_known_file_io_error
from .pytest_file_core_cases import EMBEDDED_DB_MULTI_TABLE_CASE_IDS
from .pytest_file_core_cases import EMBEDDED_DB_MULTI_TABLE_CASES
from .pytest_file_core_cases import EXPLICIT_STRING_FORMAT_CASE_IDS
from .pytest_file_core_cases import EXPLICIT_STRING_FORMAT_CASES
from .pytest_file_core_cases import FORMAT_CASES
from .pytest_file_core_cases import FORMAT_INFERENCE_CASE_IDS
from .pytest_file_core_cases import FORMAT_INFERENCE_CASES
from .pytest_file_core_cases import NUMERIC_ROUNDTRIP_NORMALIZED_FORMATS
from .pytest_file_core_cases import STUB_OPERATION_CASES
from .pytest_file_core_cases import STUBBED_FORMATS
from .pytest_file_core_cases import UNKNOWN_FORMAT_CASE_IDS
from .pytest_file_core_cases import UNKNOWN_FORMAT_CASES
from .pytest_file_core_cases import XML_ROUNDTRIP_NORMALIZED_FORMATS
from .pytest_file_core_cases import FormatPayload

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

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

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


def _assert_core_write_dispatch(
    calls: dict[str, object],
    *,
    expected_format: FileFormat,
    expected_path: Path,
    expected_data: object,
    expected_root_tag: str,
) -> None:
    """Assert core write dispatch metadata with routed XML root tag."""
    assert calls['format'] is expected_format
    assert calls['write_path'] == expected_path
    assert calls['write_data'] == expected_data

    options = calls['write_options']
    assert isinstance(options, WriteOptions)
    assert options.root_tag == expected_root_tag


def _require_case_dependencies(
    requires: tuple[str, ...],
) -> None:
    """Import case dependencies for one format case or skip."""
    for module_name in requires:
        pytest.importorskip(module_name)


def _coerce_numeric_value(value: object) -> object:
    """Coerce numeric scalars into stable Python numeric types."""
    if isinstance(value, numbers.Real):
        try:
            numeric = float(value)
            if math.isnan(numeric):
                return None
        except (TypeError, ValueError):
            return value
        if numeric.is_integer():
            return int(numeric)
        return float(numeric)
    return value


def _install_core_handler_stub(
    monkeypatch: pytest.MonkeyPatch,
    *,
    read_result: FormatPayload | None = None,
    write_result: int = 0,
) -> dict[str, object]:
    """Install a configurable core handler stub and return call metadata."""
    calls: dict[str, object] = {}

    def _at(path: Path) -> object:
        calls['bound_path'] = path

        def _read() -> FormatPayload:
            calls['read_path'] = path
            return [] if read_result is None else read_result

        def _write(
            data: object,
            *,
            options: WriteOptions | None = None,
        ) -> int:
            calls['write_path'] = path
            calls['write_data'] = data
            calls['write_options'] = options
            return write_result

        return SimpleNamespace(read=_read, write=_write)

    def _get_handler(file_format: FileFormat) -> object:
        calls['format'] = file_format
        return handler

    handler = SimpleNamespace(at=_at)

    monkeypatch.setattr(core_mod, 'get_handler', _get_handler)
    return calls


class _FakeHttpResponse:
    """Minimal HTTP response test double for File-level HTTP tests."""

    def __init__(
        self,
        *,
        status_code: int,
        payload: bytes = b'',
    ) -> None:
        self.status_code = status_code
        self.content = payload

    def close(self) -> None:
        """Close the response without side effects."""

    def raise_for_status(self) -> None:
        """Raise one error for non-successful response codes."""
        if self.status_code >= 400:
            raise RuntimeError(f'HTTP {self.status_code}')


class _FakeHttpSession:
    """Minimal session test double for end-to-end HTTP File reads."""

    def __init__(
        self,
        *,
        head_status: int = 200,
        get_status: int = 200,
        payload: bytes = b'',
    ) -> None:
        self.calls: list[tuple[str, str, bool]] = []
        self.head_status = head_status
        self.get_status = get_status
        self.payload = payload

    def close(self) -> None:
        """Close the fake session without side effects."""

    def get(self, url: str, **kwargs: Any) -> _FakeHttpResponse:
        """Return one fake GET response and capture call metadata."""
        self.calls.append(('get', url, bool(kwargs.get('stream', False))))
        return _FakeHttpResponse(
            status_code=self.get_status,
            payload=self.payload,
        )

    def head(self, url: str, **kwargs: Any) -> _FakeHttpResponse:
        """Return one fake HEAD response and capture call metadata."""
        self.calls.append(
            ('head', url, bool(kwargs.get('allow_redirects', False))),
        )
        return _FakeHttpResponse(status_code=self.head_status)


def normalize_numeric_records(records: FormatPayload) -> FormatPayload:
    """Normalize numeric values for deterministic record comparisons."""
    if not isinstance(records, list):
        return records
    return [
        (
            {key: _coerce_numeric_value(value) for key, value in row.items()}
            if isinstance(row, dict)
            else row
        )
        for row in records
    ]


def normalize_xml_payload(payload: FormatPayload) -> FormatPayload:
    """Normalize XML payloads to list-based item structures."""
    if not isinstance(payload, dict):
        return payload
    root = payload.get('root')
    if not isinstance(root, dict):
        return payload
    items = root.get('items')
    if isinstance(items, dict):
        root = {**root, 'items': [items]}
        return {**payload, 'root': root}
    return payload


def _normalize_roundtrip_values(
    *,
    file_format: FileFormat,
    result: FormatPayload,
    expected: FormatPayload,
) -> tuple[FormatPayload, FormatPayload]:
    """Normalize roundtrip values for format-specific assertion behavior."""
    normalized_result = result
    normalized_expected = expected
    if file_format in XML_ROUNDTRIP_NORMALIZED_FORMATS:
        normalized_result = normalize_xml_payload(normalized_result)
        normalized_expected = normalize_xml_payload(normalized_expected)
    if file_format in NUMERIC_ROUNDTRIP_NORMALIZED_FORMATS:
        normalized_result = normalize_numeric_records(normalized_result)
    return normalized_result, normalized_expected


def _read_with_known_io_skip(
    *,
    path: Path,
    file_format: FileFormat,
) -> FormatPayload:
    """Read one file while applying shared known I/O skip policy."""
    try:
        return cast(FormatPayload, File(path, file_format).read())
    except OSError as e:
        skip_on_known_file_io_error(
            error=e,
            file_format=file_format,
        )
        raise


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


class TestFile:
    """Unit tests for :class:`etlplus.file.File`."""

    def test_delete_delegates_to_remote_storage_backend(
        self,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Test that remote deletes delegate to the storage layer."""
        calls: list[object] = []

        class FakeBackend:
            """Remote backend delete test double."""

            def delete(self, location: object) -> None:
                """Capture the deleted location."""
                calls.append(location)

        monkeypatch.setattr(core_mod, 'get_backend', lambda value: FakeBackend())

        File('s3://bucket/data.json', FileFormat.JSON).delete()

        assert len(calls) == 1

    def test_delete_removes_local_files(
        self,
        tmp_path: Path,
    ) -> None:
        """Test that local deletes remove the underlying file."""
        path = tmp_path / 'delete_me.json'
        path.write_text('{}', encoding='utf-8')

        file = File(path, FileFormat.JSON)
        file.delete()

        assert not path.exists()

    @pytest.mark.parametrize(
        ('file_format', 'filename'),
        EMBEDDED_DB_MULTI_TABLE_CASES,
        ids=EMBEDDED_DB_MULTI_TABLE_CASE_IDS,
    )
    def test_embedded_db_read_fails_with_multiple_tables(
        self,
        tmp_path: Path,
        file_format: FileFormat,
        filename: str,
    ) -> None:
        """Test that embedded DB readers rejecting multi-table files."""
        path = tmp_path / filename
        if file_format is FileFormat.DUCKDB:
            import duckdb

            conn = cast(Any, duckdb.connect(str(path)))
        else:
            conn = cast(Any, sqlite3.connect(path))
        try:
            conn.execute('CREATE TABLE one (id INTEGER)')
            conn.execute('CREATE TABLE two (id INTEGER)')
            if file_format is FileFormat.SQLITE:
                conn.commit()
        finally:
            conn.close()

        with pytest.raises(ValueError, match='Multiple tables'):
            File(path, file_format).read()

    def test_ensure_parent_dir_creates_missing_local_parent(
        self,
        tmp_path: Path,
    ) -> None:
        """Test that local parent directories are created on demand."""
        path = tmp_path / 'nested' / 'data.json'

        File(path, FileFormat.JSON).ensure_parent_dir()

        assert path.parent.exists()

    def test_ensure_parent_dir_delegates_to_remote_storage_backend(
        self,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Test that remote parent preparation delegates to storage."""
        calls: list[object] = []

        class FakeBackend:
            """Remote backend parent-preparation test double."""

            def ensure_parent_dir(self, location: object) -> None:
                """Capture the prepared location."""
                calls.append(location)

        monkeypatch.setattr(core_mod, 'get_backend', lambda value: FakeBackend())

        File('s3://bucket/data.json', FileFormat.JSON).ensure_parent_dir()

        assert len(calls) == 1

    def test_exists_delegates_to_remote_storage_backend(
        self,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Test that remote existence checks delegate to the storage layer."""
        calls: list[object] = []

        class FakeBackend:
            """Remote backend existence test double."""

            def exists(self, location: object) -> bool:
                """Capture the location and report it as existing."""
                calls.append(location)
                return True

        monkeypatch.setattr(core_mod, 'get_backend', lambda value: FakeBackend())

        assert File('s3://bucket/data.json', FileFormat.JSON).exists() is True
        assert len(calls) == 1

    def test_exists_returns_false_for_missing_local_files(
        self,
        tmp_path: Path,
    ) -> None:
        """Test that missing local files report ``False`` for exists."""
        assert File(tmp_path / 'missing.json', FileFormat.JSON).exists() is False

    def test_exists_returns_true_for_local_files(
        self,
        tmp_path: Path,
    ) -> None:
        """Test that local existence checks use the local filesystem."""
        path = tmp_path / 'present.json'
        path.write_text('{}', encoding='utf-8')

        assert File(path, FileFormat.JSON).exists() is True

    def test_open_delegates_to_remote_storage_backend(
        self,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Test that ``File.open()`` forwards to the remote backend."""
        calls: list[tuple[object, str, dict[str, object]]] = []

        class FakeBackend:
            """Remote backend open test double."""

            def open(
                self,
                location: object,
                mode: str = 'r',
                **kwargs: object,
            ) -> BytesIO:
                """Capture the open request and return a stream."""
                calls.append((location, mode, dict(kwargs)))
                return BytesIO(b'payload')

        monkeypatch.setattr(core_mod, 'get_backend', lambda value: FakeBackend())

        with File('s3://bucket/data.bin').open('rb') as handle:
            assert handle.read() == b'payload'

        assert len(calls) == 1
        assert calls[0][1] == 'rb'

    def test_open_uses_local_backend_for_text_reads(
        self,
        tmp_path: Path,
    ) -> None:
        """Test that ``File.open()`` reads local text content."""
        path = tmp_path / 'data.txt'
        path.write_text('alpha', encoding='utf-8')

        with File(path).open(encoding='utf-8') as handle:
            assert handle.read() == 'alpha'

    def test_read_bytes_delegates_to_remote_storage_backend(
        self,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Test that ``File.read_bytes()`` uses the storage backend."""
        calls: list[tuple[object, str]] = []

        class FakeBackend:
            """Remote backend read-bytes test double."""

            def open(
                self,
                location: object,
                mode: str = 'r',
                **kwargs: object,
            ) -> BytesIO:
                """Capture the open request and return a binary stream."""
                del kwargs
                calls.append((location, mode))
                return BytesIO(b'payload')

        monkeypatch.setattr(core_mod, 'get_backend', lambda value: FakeBackend())

        assert File('s3://bucket/data.bin').read_bytes() == b'payload'
        assert len(calls) == 1
        assert calls[0][1] == 'rb'

    def test_read_bytes_uses_local_backend(
        self,
        tmp_path: Path,
    ) -> None:
        """Test that ``File.read_bytes()`` returns local binary content."""
        path = tmp_path / 'data.bin'
        path.write_bytes(b'payload')

        assert File(path).read_bytes() == b'payload'

    def test_touch_creates_missing_local_file(
        self,
        tmp_path: Path,
    ) -> None:
        """Test that local touch creates missing files and parents."""
        path = tmp_path / 'nested' / 'data.json'

        File(path, FileFormat.JSON).touch()

        assert path.exists()
        assert path.read_text(encoding='utf-8') == ''

    def test_write_bytes_delegates_to_remote_storage_backend(
        self,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Test that ``File.write_bytes()`` uses the storage backend."""
        uploads: list[bytes] = []

        class CaptureUpload(BytesIO):
            """Writable remote upload stream test double."""

            def close(self) -> None:
                """Capture uploaded bytes before closing the stream."""
                uploads.append(self.getvalue())
                super().close()

        class FakeBackend:
            """Remote backend write-bytes test double."""

            def open(
                self,
                location: object,
                mode: str = 'r',
                **kwargs: object,
            ) -> CaptureUpload:
                """Capture the write open request and return a binary sink."""
                del location, kwargs
                assert mode == 'wb'
                return CaptureUpload()

        monkeypatch.setattr(core_mod, 'get_backend', lambda value: FakeBackend())

        File('s3://bucket/data.bin').write_bytes(b'payload')

        assert uploads == [b'payload']

    def test_write_bytes_uses_local_backend(
        self,
        tmp_path: Path,
    ) -> None:
        """Test that ``File.write_bytes()`` writes local binary content."""
        path = tmp_path / 'data.bin'

        File(path).write_bytes(b'payload')

        assert path.read_bytes() == b'payload'

    def test_touch_remote_creates_empty_object_when_missing(
        self,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Test that remote touch creates an empty object when absent."""
        uploads: list[bytes] = []

        class CaptureUpload(BytesIO):
            """Writable remote upload stream test double."""

            def close(self) -> None:
                """Capture uploaded bytes before closing the stream."""
                uploads.append(self.getvalue())
                super().close()

        class FakeBackend:
            """Remote backend touch test double."""

            def __init__(self) -> None:
                self.parent_calls = 0
                self.exists_calls = 0

            def ensure_parent_dir(self, location: object) -> None:
                """Record parent preparation for the new object."""
                del location
                self.parent_calls += 1

            def exists(self, location: object) -> bool:
                """Report the object as missing on first touch."""
                del location
                self.exists_calls += 1
                return False

            def open(
                self,
                location: object,
                mode: str = 'r',
                **kwargs: object,
            ) -> CaptureUpload:
                """Return a writable stream for the new object."""
                del location, kwargs
                assert mode == 'wb'
                return CaptureUpload()

        backend = FakeBackend()
        monkeypatch.setattr(core_mod, 'get_backend', lambda value: backend)

        File('s3://bucket/data.json', FileFormat.JSON).touch()

        assert backend.exists_calls == 1
        assert backend.parent_calls == 1
        assert uploads == [b'']

    def test_touch_remote_is_noop_when_object_exists(
        self,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Test that remote touch avoids truncating existing objects."""
        calls: list[str] = []

        class FakeBackend:
            """Remote backend touch no-op test double."""

            def exists(self, location: object) -> bool:
                """Report the object as already present."""
                del location
                calls.append('exists')
                return True

            def ensure_parent_dir(self, location: object) -> None:
                """Fail if parent preparation happens unexpectedly."""
                raise AssertionError('ensure_parent_dir should not be called')

            def open(
                self,
                location: object,
                mode: str = 'r',
                **kwargs: object,
            ) -> BytesIO:
                """Fail if open happens unexpectedly."""
                raise AssertionError('open should not be called')

        monkeypatch.setattr(core_mod, 'get_backend', lambda value: FakeBackend())

        File('s3://bucket/data.json', FileFormat.JSON).touch()

        assert calls == ['exists']

    @pytest.mark.parametrize(
        ('raw_format', 'expected'),
        EXPLICIT_STRING_FORMAT_CASES,
        ids=EXPLICIT_STRING_FORMAT_CASE_IDS,
    )
    def test_explicit_string_file_format_validation(
        self,
        tmp_path: Path,
        raw_format: str,
        expected: FileFormat | None,
    ) -> None:
        """Test that explicit string file-format coercion and validation."""
        path = tmp_path / 'data.json'
        if expected is None:
            with pytest.raises(ValueError, match='Invalid FileFormat value'):
                File(path, cast(Any, raw_format))
            return
        file = File(path, cast(Any, raw_format))
        assert file.file_format is expected

    def test_gz_roundtrip_json(
        self,
        tmp_path: Path,
    ) -> None:
        """Test that JSON round trip inside a gzip archive."""
        path = tmp_path / 'data.json.gz'
        payload = [{'name': 'Ada'}]

        File(path, FileFormat.GZ).write(payload)
        result = File(path, FileFormat.GZ).read()

        assert result == payload

    def test_http_read_uses_real_backend_path_via_requests_session(
        self,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Test that HTTP File reads flow through the real HTTP backend."""
        session = _FakeHttpSession(
            payload=b'{"name": "Ada"}',
        )

        monkeypatch.setattr(http_storage_mod.requests, 'Session', lambda: session)

        file = File('https://example.com/files/data.json?download=1')
        result = file.read()

        assert file.file_format is FileFormat.JSON
        assert file.location.scheme is StorageScheme.HTTP
        assert result == {'name': 'Ada'}
        assert session.calls == [
            ('head', 'https://example.com/files/data.json?download=1', True),
            ('get', 'https://example.com/files/data.json?download=1', False),
        ]

    def test_http_uri_infers_http_backend_and_csv_format(self) -> None:
        """Test that generic HTTPS URIs infer HTTP storage and CSV format."""
        file = File('https://example.com/files/my_file.csv?download=1')

        assert file.file_format is FileFormat.CSV
        assert file.location.scheme is StorageScheme.HTTP
        assert file.path == 'https://example.com/files/my_file.csv?download=1'

    @pytest.mark.parametrize(
        ('filename', 'expected_format'),
        FORMAT_INFERENCE_CASES,
        ids=FORMAT_INFERENCE_CASE_IDS,
    )
    def test_infers_format_from_extension_patterns(
        self,
        tmp_path: Path,
        filename: str,
        expected_format: FileFormat,
    ) -> None:
        """
        Test that inference from standard and compressed filename patterns.
        """
        path = tmp_path / filename
        path.write_text('{}', encoding='utf-8')

        file = File(path)

        assert file.file_format == expected_format

    def test_path_property_reflects_local_location(self, tmp_path: Path) -> None:
        """Test that local files expose a :class:`Path` via ``file.path``."""
        path = tmp_path / 'data.json'

        file = File(path)

        assert file.location.is_local
        assert file.path == path

    def test_path_property_reflects_remote_location(self) -> None:
        """Test that remote files expose the original URI via ``file.path``."""
        uri = 's3://bucket/data.json'

        file = File(uri, FileFormat.JSON)

        assert not file.location.is_local
        assert file.path == uri

    def test_path_support_for_module_helpers(
        self,
        tmp_path: Path,
    ) -> None:
        """Test that module helpers accept ``Path`` inputs."""
        cases: tuple[
            tuple[Any, str, JSONData, JSONData, dict[str, object]],
            ...,
        ] = (
            (
                csv_file.CsvFile(),
                'data.csv',
                [{'name': 'Ada'}],
                [{'name': 'Ada'}],
                {},
            ),
            (
                json_file.JsonFile(),
                'data.json',
                {'name': 'Ada'},
                {'name': 'Ada'},
                {},
            ),
            (
                xml_file.XmlFile(),
                'data.xml',
                {'root': {'text': 'hello'}},
                {'root': {'text': 'hello'}},
                {'options': WriteOptions(root_tag='root')},
            ),
        )
        for handler, filename, payload, expected, write_kwargs in cases:
            path = tmp_path / filename
            handler.write(path, payload, **write_kwargs)
            assert handler.read(path) == expected

    def test_remote_read_stages_payload_through_storage_backend(
        self,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Test that remote reads download to a local temp path first."""
        calls: list[tuple[str, str]] = []

        class FakeBackend:
            """Remote backend read test double."""

            def exists(self, location: object) -> bool:
                """Report the remote object as present."""
                calls.append(('exists', str(location)))
                return True

            def open(
                self,
                location: object,
                mode: str = 'r',
                **kwargs: object,
            ) -> BytesIO:
                """Return a readable byte stream for the remote object."""
                del kwargs
                calls.append((mode, str(location)))
                return BytesIO(b'{"name": "Ada"}')

        monkeypatch.setattr(core_mod, 'get_backend', lambda value: FakeBackend())

        result = File('s3://bucket/data.json', FileFormat.JSON).read()

        assert result == {'name': 'Ada'}
        assert calls[0][0] == 'exists'
        assert calls[1][0] == 'rb'

    def test_remote_uri_infers_s3_backend_and_csv_format(self) -> None:
        """Test that remote URI strings infer both storage scheme and format."""
        file = File('s3://my-bucket/my_file.csv')

        assert file.file_format is FileFormat.CSV
        assert file.location.scheme is StorageScheme.S3
        assert isinstance(get_backend(file.location), S3StorageBackend)
        assert file.path == 's3://my-bucket/my_file.csv'

    def test_remote_write_uploads_staged_payload_via_storage_backend(
        self,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Test that remote writes upload the local staged file content."""
        uploads: list[bytes] = []

        class CaptureUpload(BytesIO):
            """Writable remote upload stream test double."""

            def close(self) -> None:
                """Capture uploaded bytes before closing the stream."""
                uploads.append(self.getvalue())
                super().close()

        class FakeBackend:
            """Remote backend write test double."""

            def ensure_parent_dir(self, location: object) -> None:
                """Accept parent preparation for remote storage."""
                del location

            def open(
                self,
                location: object,
                mode: str = 'r',
                **kwargs: object,
            ) -> CaptureUpload:
                """Return a writable byte stream for the remote object."""
                del location, kwargs
                assert mode == 'wb'
                return CaptureUpload()

        monkeypatch.setattr(core_mod, 'get_backend', lambda value: FakeBackend())

        written = File('s3://bucket/data.json', FileFormat.JSON).write(
            {'name': 'Ada'},
        )

        assert written == 1
        assert uploads == [b'{\n  "name": "Ada"\n}\n']

    def test_read_csv_skips_blank_rows(
        self,
        tmp_path: Path,
    ) -> None:
        """Test that CSV reader ignoring empty rows."""
        payload = 'name,age\nJohn,30\n,\nJane,25\n'
        path = tmp_path / 'data.csv'
        path.write_text(payload, encoding='utf-8')

        rows = File(path, FileFormat.CSV).read()

        assert [
            row['name'] for row in rows if isinstance(row, dict) and 'name' in row
        ] == ['John', 'Jane']

    def test_read_json_type_errors(self, tmp_path: Path) -> None:
        """Test that list elements being dicts when reading JSON."""
        path = tmp_path / 'bad.json'
        path.write_text('[{"ok": 1}, 2]', encoding='utf-8')

        with pytest.raises(TypeError):
            File(path, FileFormat.JSON).read()

    def test_read_missing_file_raises_before_format_inference(
        self,
        tmp_path: Path,
    ) -> None:
        """
        Test that missing path checks run before format inference on read.
        """
        missing = tmp_path / 'missing.unknown'
        file = File(missing)

        with pytest.raises(FileNotFoundError):
            file.read()

    @pytest.mark.parametrize(
        ('filename', 'contents', 'operation', 'error_pattern'),
        UNKNOWN_FORMAT_CASES,
        ids=UNKNOWN_FORMAT_CASE_IDS,
    )
    def test_unknown_formats_defer_error(
        self,
        tmp_path: Path,
        filename: str,
        contents: str | None,
        operation: Operation,
        error_pattern: str,
    ) -> None:
        """Test that unresolved formats deferring failure until dispatch."""
        path = tmp_path / filename
        if contents is not None:
            path.write_text(contents, encoding='utf-8')
        file = File(path)

        assert file.file_format is None
        operation_kwargs = {
            'read': {},
            'write': {'data': {'ok': True}},
        }[operation]
        with pytest.raises(ValueError, match=error_pattern):
            getattr(file, operation)(**operation_kwargs)

    @pytest.mark.parametrize(
        ('file_format', 'filename', 'payload', 'expected', 'requires'),
        [
            pytest.param(
                file_format,
                filename,
                payload,
                expected,
                requires,
                id=file_format.value,
            )
            for (
                file_format,
                filename,
                payload,
                expected,
                requires,
            ) in FORMAT_CASES
        ],
    )
    def test_roundtrip_by_format(
        self,
        tmp_path: Path,
        file_format: FileFormat,
        filename: str,
        payload: FormatPayload,
        expected: FormatPayload,
        requires: tuple[str, ...],
    ) -> None:
        """Test that round-trip reads and writes across file formats."""
        _require_case_dependencies(requires)
        path = tmp_path / filename

        File(path, file_format).write(payload)
        result = _read_with_known_io_skip(
            path=path,
            file_format=file_format,
        )
        normalized_result, normalized_expected = _normalize_roundtrip_values(
            file_format=file_format,
            result=result,
            expected=expected,
        )
        assert normalized_result == normalized_expected

    @pytest.mark.parametrize(
        ('file_format', 'filename'),
        [
            pytest.param(
                file_format,
                filename,
                id=file_format.value,
            )
            for file_format, filename in STUBBED_FORMATS
        ],
    )
    @pytest.mark.parametrize('operation', STUB_OPERATION_CASES)
    def test_stub_formats_raise_on_operations(
        self,
        tmp_path: Path,
        file_format: FileFormat,
        filename: str,
        operation: Operation,
    ) -> None:
        """
        Test that stub formats raising :class:`NotImplementedError` on
        read/write.
        """
        path = tmp_path / filename
        seed_content = {'read': 'stub', 'write': None}[operation]
        if seed_content is not None:
            path.write_text(seed_content, encoding='utf-8')

        file = File(path, file_format)
        operation_kwargs = {
            'read': {},
            'write': {'data': {'stub': True}},
        }[operation]
        with pytest.raises(NotImplementedError):
            getattr(file, operation)(**operation_kwargs)

    def test_write_csv_rejects_non_dicts(
        self,
        tmp_path: Path,
    ) -> None:
        """
        Test that non-dict entries raising a :class:`TypeError` when writing
        CSV rows.
        """
        path = tmp_path / 'data.csv'
        invalid_entry = cast(dict[str, object], 'invalid')
        with pytest.raises(TypeError, match='CSV payloads'):
            File(path, FileFormat.CSV).write(
                [{'name': 'John'}, invalid_entry],
            )

    def test_write_json_returns_record_count(
        self,
        tmp_path: Path,
    ) -> None:
        """
        Test that :meth:`write` returns the record count for lists.
        """
        path = tmp_path / 'data.json'
        records = [{'a': 1}, {'a': 2}]

        written = File(path, FileFormat.JSON).write(records)

        assert written == 2
        json_content = path.read_text(encoding='utf-8')
        assert json_content
        assert json_content.count('\n') >= 2

    def test_xls_write_not_supported(
        self,
        tmp_path: Path,
    ) -> None:
        """
        Test that :func:`write` raises an error indicating lack of support.
        """
        path = tmp_path / 'sample.xls'

        with pytest.raises(RuntimeError, match='read-only'):
            File(path, FileFormat.XLS).write([{'name': 'Ada', 'age': 36}])

    def test_xml_respects_root_tag(
        self,
        tmp_path: Path,
    ) -> None:
        """
        Test that custom root_tag being used when data lacks a single root.
        """
        path = tmp_path / 'export.xml'
        records = [{'name': 'Ada'}, {'name': 'Linus'}]

        File(path, FileFormat.XML).write(records, root_tag='records')

        text = path.read_text(encoding='utf-8')
        assert text.startswith('<?xml')
        assert '<records>' in text

    def test_xml_roundtrip_with_attributes(
        self,
        tmp_path: Path,
    ) -> None:
        """Test that XML read/write preserves attribute fields."""
        path = tmp_path / 'attrs.xml'
        payload: JSONData = {
            'root': {
                '@id': '42',
                'item': {'@lang': 'en', 'text': 'Hello'},
            },
        }

        File(path, FileFormat.XML).write(payload)
        result = File(path, FileFormat.XML).read()

        assert result == payload

    def test_zip_multi_file_read(
        self,
        tmp_path: Path,
    ) -> None:
        """
        Test that ZIP files with multiple entries return a dict payload.
        """
        path = tmp_path / 'bundle.zip'
        with zipfile.ZipFile(path, 'w') as archive:
            archive.writestr('a.json', '{"a": 1}')
            archive.writestr('b.json', '{"b": 2}')

        result = File(path, FileFormat.ZIP).read()

        assert result == {'a.json': {'a': 1}, 'b.json': {'b': 2}}


class TestFileCoreDispatch:
    """Unit tests for class-based dispatch in :class:`etlplus.file.File`."""

    def test_read_uses_class_based_handler_dispatch(
        self,
        tmp_path: Path,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """
        Test that read dispatch through :func:`core.get_handler` handlers.
        """
        path = tmp_path / 'sample.csv'
        path.write_text('name\nAda\n', encoding='utf-8')
        read_result: JSONData = {'ok': True}
        calls = _install_core_handler_stub(
            monkeypatch,
            read_result=read_result,
        )

        result = File(path, FileFormat.CSV).read()

        assert result == read_result
        assert calls['format'] is FileFormat.CSV
        assert calls['bound_path'] == path
        assert calls['read_path'] == path

    @pytest.mark.parametrize(
        ('root_tag', 'expected_root_tag', 'write_result'),
        [
            pytest.param(
                None,
                xml_file.DEFAULT_XML_ROOT,
                1,
                id='default_root_tag',
            ),
            pytest.param(
                'records',
                'records',
                3,
                id='custom_root_tag',
            ),
        ],
    )
    def test_write_uses_class_based_handler_and_root_tag(
        self,
        tmp_path: Path,
        monkeypatch: pytest.MonkeyPatch,
        root_tag: str | None,
        expected_root_tag: str,
        write_result: int,
    ) -> None:
        """Test that write dispatch preserving XML *root_tag* in options."""
        path = tmp_path / 'export.xml'
        payload: JSONData = [{'name': 'Ada'}]
        calls = _install_core_handler_stub(
            monkeypatch,
            write_result=write_result,
        )

        file = File(path, FileFormat.XML)
        if root_tag is None:
            written = file.write(payload)
        else:
            written = file.write(payload, root_tag=root_tag)

        assert written == write_result
        _assert_core_write_dispatch(
            calls,
            expected_format=FileFormat.XML,
            expected_path=path,
            expected_data=payload,
            expected_root_tag=expected_root_tag,
        )
        assert calls['bound_path'] == path
