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

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

from __future__ import annotations

from pathlib import Path
from typing import Any

import pytest

from etlplus.file import FileFormat
from etlplus.file import _stub_categories as mod
from etlplus.file.base import ReadOptions
from etlplus.file.base import WriteOptions
from etlplus.file.stub import StubFileHandlerABC
from etlplus.utils.types import JSONData
from etlplus.utils.types import JSONList

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

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

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


RECORD_LIST: JSONData = [{'id': 1}]
RECORD_ROWS: JSONList = [{'id': 1}]
EVENT_DICT: JSONData = {'event': 'ok'}
OK_DICT: JSONData = {'ok': True}


def _patch_stub_read(
    monkeypatch: pytest.MonkeyPatch,
    *,
    result: Any,
    expected_path: Path | None = None,
) -> list[tuple[Path, ReadOptions | None]]:
    """Patch ``StubFileHandlerABC.read`` and collect read call metadata."""
    read_calls: list[tuple[Path, ReadOptions | None]] = []

    def _read(
        self: StubFileHandlerABC,
        path: Path,
        *,
        options: ReadOptions | None = None,
    ) -> JSONData:
        if expected_path is not None:
            assert path == expected_path
        read_calls.append((path, options))
        return result

    monkeypatch.setattr(StubFileHandlerABC, 'read', _read)
    return read_calls


def _patch_stub_write(
    monkeypatch: pytest.MonkeyPatch,
    *,
    result: int,
) -> list[tuple[Path, JSONData, WriteOptions | None]]:
    """Patch ``StubFileHandlerABC.write`` and collect write call metadata."""
    write_calls: list[tuple[Path, JSONData, WriteOptions | None]] = []

    def _write(
        self: StubFileHandlerABC,
        path: Path,
        data: JSONData,
        *,
        options: WriteOptions | None = None,
    ) -> int:
        write_calls.append((path, data, options))
        return result

    monkeypatch.setattr(StubFileHandlerABC, 'write', _write)
    return write_calls


class _BinaryStub(mod.StubBinarySerializationFileHandlerABC):
    """Concrete binary stub for unit tests."""

    format = FileFormat.CBOR


class _EmbeddedStub(mod.StubEmbeddedDatabaseFileHandlerABC):
    """Concrete embedded-database stub for unit tests."""

    format = FileFormat.ACCDB


class _LogStub(mod.StubLogEventFileHandlerABC):
    """Concrete log-event stub for unit tests."""

    format = FileFormat.LOG


class _SemiStructuredStub(mod.StubSemiStructuredTextFileHandlerABC):
    """Concrete semi-structured stub for unit tests."""

    format = FileFormat.CONF


class _SingleScientificStub(mod.StubSingleDatasetScientificFileHandlerABC):
    """Concrete single-dataset scientific stub for unit tests."""

    format = FileFormat.MAT


class _SpreadsheetStub(mod.StubSpreadsheetFileHandlerABC):
    """Concrete spreadsheet stub for unit tests."""

    format = FileFormat.WKS


class _TemplateStub(mod.StubTemplateFileHandlerABC):
    """Concrete template stub for unit tests."""

    format = FileFormat.JINJA2


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


class TestStubCategoryHandlers:
    """Unit tests for category-specific stub handler ABCs."""

    def test_binary_stub_loads_and_dumps_delegate_to_stub_io(
        self,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """
        Test that binary payload methods delegates to
        :class:`StubFileHandlerABC`.
        """
        read_calls = _patch_stub_read(
            monkeypatch,
            result=RECORD_LIST,
        )
        write_calls = _patch_stub_write(monkeypatch, result=1)
        handler = _BinaryStub()

        assert handler.loads_bytes(b'payload') == [{'id': 1}]
        assert handler.dumps_bytes([{'id': 1}]) == b''
        assert read_calls == [(Path('ignored.cbor'), None)]
        assert write_calls == [(Path('ignored.cbor'), [{'id': 1}], None)]

    def test_embedded_stub_methods_delegate_to_stub_io(
        self,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """
        Test that embedded-database methods delegates to
        :class:`StubFileHandlerABC`.
        """
        source_path = Path('data.accdb')
        read_calls = _patch_stub_read(
            monkeypatch,
            result=RECORD_LIST,
        )
        write_calls = _patch_stub_write(monkeypatch, result=3)
        handler = _EmbeddedStub()

        assert handler.connect(source_path) == [{'id': 1}]
        assert handler.list_tables(object()) == [{'id': 1}]
        assert handler.read(source_path) == [{'id': 1}]
        assert handler.read_table(object(), 'events') == [{'id': 1}]
        assert handler.write(source_path, [{'id': 1}]) == 3
        assert handler.write_table(object(), 'events', [{'id': 1}]) == 3
        assert [path for path, _ in read_calls] == [
            source_path,
            Path('ignored.accdb'),
            source_path,
            Path('ignored.accdb'),
        ]
        assert [(path, data) for path, data, _ in write_calls] == [
            (source_path, RECORD_LIST),
            (Path('ignored.accdb'), RECORD_LIST),
        ]

    def test_log_stub_parse_and_serialize_delegate_to_stub_io(
        self,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """
        Test that log-event parse/serialize methods delegating to stub IO.
        """
        _patch_stub_read(
            monkeypatch,
            result=EVENT_DICT,
            expected_path=Path('ignored.log'),
        )
        write_calls = _patch_stub_write(monkeypatch, result=1)
        handler = _LogStub()

        assert handler.parse_line('hello') == {'event': 'ok'}
        assert handler.serialize_event({'event': 'ok'}) == ''
        assert [(path, data) for path, data, _ in write_calls] == [
            (Path('ignored.log'), EVENT_DICT),
        ]

    def test_semistructured_stub_methods_delegate_to_stub_io(
        self,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """
        Test that semi-structured stub read/write/loads/dumps delegation.
        """
        source_path = Path('data.conf')
        read_calls = _patch_stub_read(
            monkeypatch,
            result=OK_DICT,
        )
        write_calls = _patch_stub_write(monkeypatch, result=2)
        handler = _SemiStructuredStub()

        assert handler.read(source_path) == OK_DICT
        assert handler.loads('ignored') == OK_DICT
        assert handler.write(source_path, OK_DICT) == 2
        assert handler.dumps(OK_DICT) == ''
        assert [path for path, _ in read_calls] == [
            source_path,
            Path('ignored.conf'),
        ]
        assert [(path, data) for path, data, _ in write_calls] == [
            (source_path, OK_DICT),
            (Path('ignored.conf'), OK_DICT),
        ]

    def test_single_dataset_stub_validates_dataset_key(
        self,
    ) -> None:
        """Test that single-dataset stub rejects non-default dataset keys."""
        handler = _SingleScientificStub()
        path = Path('data.mat')
        with pytest.raises(ValueError, match='supports only dataset key'):
            handler.read_dataset(path, dataset='other')
        with pytest.raises(ValueError, match='supports only dataset key'):
            handler.write_dataset(path, [], dataset='other')

    def test_single_dataset_stub_read_write_delegate_for_default_dataset(
        self,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """
        Test that single-dataset stub read/write delegation for valid dataset.
        """
        read_calls = _patch_stub_read(
            monkeypatch,
            result=RECORD_LIST,
        )
        write_calls = _patch_stub_write(monkeypatch, result=1)
        handler = _SingleScientificStub()
        path = Path('data.mat')
        read_options = ReadOptions(dataset='data')
        write_options = WriteOptions(dataset='data')

        assert handler.read(path, options=read_options) == RECORD_LIST
        assert handler.write(path, RECORD_LIST, options=write_options) == 1
        assert read_calls == [(path, read_options)]
        assert write_calls == [(path, RECORD_LIST, write_options)]

    def test_spreadsheet_stub_methods_delegate_to_stub_io(
        self,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """
        Test that spreadsheet stub methods forwarding to
        :class:`StubFileHandlerABC`.
        """
        path = Path('book.wks')
        read_calls = _patch_stub_read(
            monkeypatch,
            result=RECORD_LIST,
        )
        write_calls = _patch_stub_write(monkeypatch, result=4)
        handler = _SpreadsheetStub()

        assert handler.engine_name == 'stub'
        assert handler.read(path) == RECORD_LIST
        assert handler.read_sheet(path, sheet='Sheet1') == RECORD_LIST
        assert handler.write(path, RECORD_ROWS) == 4
        assert handler.write_sheet(path, RECORD_ROWS, sheet='Sheet1') == 4
        assert [read_path for read_path, _ in read_calls] == [path, path]
        assert [
            (write_path, write_data) for write_path, write_data, _ in write_calls
        ] == [
            (path, RECORD_LIST),
            (path, RECORD_LIST),
        ]

    def test_template_stub_render_delegates_to_stub_read(
        self,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Test template-stub rendering delegation to stub read behavior."""
        _patch_stub_read(
            monkeypatch,
            result='rendered',
            expected_path=Path('ignored.jinja2'),
        )
        handler = _TemplateStub()

        assert handler.template_engine == 'stub'
        assert handler.render('Hi {{ name }}', {'name': 'Ada'}) == 'rendered'
