"""
:mod:`tests.unit.test_u_transform` module.

Unit tests for ``etlplus.transform``.

Notes
-----
- Uses small in-memory datasets to validate each operation.
- Ensures stable behavior for edge cases (empty inputs, missing fields).
"""

from __future__ import annotations

from collections.abc import Callable

import pytest

from etlplus.transform import apply_aggregate
from etlplus.transform import apply_filter
from etlplus.transform import apply_map
from etlplus.transform import apply_select
from etlplus.transform import apply_sort
from etlplus.transform import transform

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


@pytest.mark.unit
class TestApplyAggregate:
    """Unit test suite for :func:`etlplus.transform.apply_aggregate`."""

    @pytest.mark.parametrize(
        'func, expected_result',
        [
            ('avg', 15),
            ('count', 3),
            ('min', 10),
            ('max', 20),
            ('sum', 45),
        ],
    )
    def test_aggregate(
        self,
        func: str,
        expected_result: int,
    ) -> None:
        """
        Test aggregating the ``value`` field with built-in functions.

        Parameters
        ----------
        func : str
            Aggregation function to apply.
        expected_result : int
            Expected result of the aggregation.
        """
        data = [
            {'name': 'John', 'value': 10},
            {'name': 'Jane', 'value': 20},
            {'name': 'Bob', 'value': 15},
        ]
        result = apply_aggregate(data, {'field': 'value', 'func': func})
        key = f'{func}_value' if func != 'count' else 'count_value'
        assert result[key] == expected_result

    def test_aggregate_callable_with_alias(self) -> None:
        """Test aggregating with a callable and custom alias."""

        def score(nums: list[float], present: int) -> float:
            return sum(nums) + present

        data = [
            {'value': 10},
            {'value': 20},
            {'value': 15},
        ]
        result = apply_aggregate(
            data,
            {
                'field': 'value',
                'func': score,
                'alias': 'score',
            },
        )
        assert result == {'score': 48}


@pytest.mark.unit
class TestApplyFilter:
    """Unit test suite for :func:`etlplus.transform.apply_filter`."""

    @pytest.mark.parametrize(
        'data, op, value, expected_names',
        [
            (
                [
                    {'name': 'John'},
                    {'name': 'Jane'},
                    {'name': 'Bob'},
                ],
                lambda v, n: n in v.lower(),
                'a',
                ['Jane'],
            ),
        ],
    )
    def test_filter_callable_operator(
        self,
        data: list[dict[str, str]],
        op: Callable[[str, str], bool],
        value: str,
        expected_names: list[str],
    ) -> None:
        """
        Test filtering with a custom callable operator.

        Parameters
        ----------
        data : list[dict[str, str]]
            Input records.
        op : Callable[[str, str], bool]
            Operator function.
        value : str
            Value to filter by.
        expected_names : list[str]
            Expected names after filter.
        """
        result = apply_filter(
            data,
            {
                'field': 'name',
                'op': op,
                'value': value,
            },
        )
        assert [item['name'] for item in result] == expected_names

    @pytest.mark.parametrize(
        'data, op, value, expected_count',
        [
            (
                [
                    {'name': 'John', 'age': 30},
                    {'name': 'Jane', 'age': 25},
                    {'name': 'Bob', 'age': 30},
                ],
                'eq',
                30,
                2,
            ),
            (
                [
                    {'name': 'John', 'age': 30},
                    {'name': 'Jane', 'age': 25},
                    {'name': 'Bob', 'age': 35},
                ],
                'gt',
                28,
                2,
            ),
            (
                [
                    {'name': 'John', 'age': '30'},
                    {'name': 'Jane', 'age': '25'},
                ],
                'gt',
                26,
                1,
            ),
        ],
    )
    def test_filter_numeric_ops(
        self,
        data: list[dict],
        op: str,
        value: int | str,
        expected_count: int,
    ) -> None:
        """
        Test filtering with numeric operators.

        Parameters
        ----------
        data : list[dict]
            Input records.
        op : str
            Operator name.
        value : int | str
            Value to filter by.
        expected_count : int
            Expected number of filtered records.
        """
        result = apply_filter(data, {'field': 'age', 'op': op, 'value': value})
        assert len(result) == expected_count

    def test_apply_filter_in(self) -> None:
        """
        Test filtering with the ``in`` operator.

        Notes
        -----
        Keeps records whose ``status`` is in the provided list.
        """
        data = [
            {'name': 'John', 'status': 'active'},
            {'name': 'Jane', 'status': 'inactive'},
            {'name': 'Bob', 'status': 'active'},
        ]
        result = apply_filter(
            data,
            {
                'field': 'status',
                'op': 'in',
                'value': ['active', 'pending'],
            },
        )
        assert len(result) == 2


@pytest.mark.unit
class TestApplyMap:
    """Unit test suite for :func:`etlplus.transform.apply_map`."""

    def test_map(self) -> None:
        """Test mapping/renaming fields in each record."""
        data = [
            {'old_name': 'John', 'age': 30},
            {'old_name': 'Jane', 'age': 25},
        ]
        result = apply_map(data, {'old_name': 'new_name'})
        assert all('new_name' in item for item in result)
        assert all('old_name' not in item for item in result)
        assert result[0]['new_name'] == 'John'


@pytest.mark.unit
class TestApplySelect:
    """Unit test suite for :func:`etlplus.transform.apply_select`."""

    def test_select(self) -> None:
        """
        Test selecting a subset of fields from each record.

        Notes
        -----
        Retains only ``name`` and ``age``.
        """
        data = [
            {'name': 'John', 'age': 30, 'city': 'NYC'},
            {'name': 'Jane', 'age': 25, 'city': 'LA'},
        ]
        result = apply_select(data, ['name', 'age'])
        assert all(set(item.keys()) == {'name', 'age'} for item in result)


@pytest.mark.unit
class TestApplySort:
    """Unit test suite for :func:`etlplus.transform.apply_sort`."""

    @pytest.mark.parametrize(
        'reverse, expected_sorted_ages',
        [
            (False, [25, 30, 35]),
            (True, [35, 30, 25]),
        ],
    )
    def test_sort(
        self,
        reverse: bool,
        expected_sorted_ages: list[int],
    ) -> None:
        """
        Test sorting records by a field.

        Parameters
        ----------
        reverse : bool
            Whether to sort in descending order.
        expected_sorted_ages : list[int]
            Expected sorted ages.

        Notes
        -----
        Checks ascending and descending sort by ``age``.
        """
        data = [
            {'name': 'John', 'age': 30},
            {'name': 'Jane', 'age': 25},
            {'name': 'Bob', 'age': 35},
        ]
        result = apply_sort(data, 'age', reverse=reverse)
        assert [item['age'] for item in result] == expected_sorted_ages


@pytest.mark.unit
class TestTransform:
    """Unit test suite for :func:`etlplus.transform.transform`."""

    def test_from_json_string(self) -> None:
        """
        Test transforming from a JSON string.

        Notes
        -----
        Selects only ``name`` from the provided JSON array string.
        """
        json_str = '[{"name": "John", "age": 30}]'
        result = transform(json_str, {'select': ['name']})
        assert len(result) == 1
        assert 'age' not in result

    def test_from_file(
        self,
        temp_json_file: Callable[[list[dict]], str],
    ) -> None:
        """
        Test transforming from a JSON file.

        Parameters
        ----------
        temp_json_file : Callable[[list[dict]], str]
            Fixture to create a temp JSON file.

        Notes
        -----
        Writes a temporary JSON file and selects only ``name``.
        """
        temp_path = temp_json_file([{'name': 'John', 'age': 30}])
        result = transform(temp_path, {'select': ['name']})
        assert len(result) == 1
        assert 'age' not in result

    def test_no_operations(self) -> None:
        """Test transforming without operations returns input unchanged."""
        data = [{'name': 'John'}]
        result = transform(data)
        assert result == data

    def test_with_aggregate(self) -> None:
        """
        Test transforming using an aggregate operation.

        Notes
        -----
        Sums the ``value`` field across records.
        """
        data = [
            {'name': 'John', 'value': 10},
            {'name': 'Jane', 'value': 20},
        ]
        result = transform(
            data,
            {'aggregate': {'field': 'value', 'func': 'sum'}},
        )
        assert isinstance(result, dict)
        assert len(result) == 1
        assert result['sum_value'] == 30

    def test_with_filter(self) -> None:
        """
        Test transforming using a filter operation.

        Notes
        -----
        Filters for ``age > 26``.
        """
        data = [
            {'name': 'John', 'age': 30},
            {'name': 'Jane', 'age': 25},
        ]
        result = transform(
            data,
            {
                'filter': {
                    'field': 'age',
                    'op': 'gt',
                    'value': 26,
                },
            },
        )
        assert isinstance(result, list)
        assert len(result) == 1
        assert result[0]['name'] == 'John'

    def test_with_map(self) -> None:
        """
        Test transforming using a map operation.

        Notes
        -----
        Renames ``old_field`` to ``new_field``.
        """
        data = [{'old_field': 'value'}]
        result = transform(data, {'map': {'old_field': 'new_field'}})
        assert isinstance(result, list)
        assert len(result) == 1
        assert 'new_field' in result[0]

    def test_with_multiple_aggregates(self) -> None:
        """
        Transform with multiple aggregations.

        Notes
        -----
        Produces both sum and count results.
        """
        data = [
            {'value': 1},
            {'value': 2},
            {'value': 3},
        ]
        result = transform(
            data,
            {
                'aggregate': [
                    {'field': 'value', 'func': 'sum'},
                    {'field': 'value', 'func': 'count', 'alias': 'count'},
                ],
            },
        )
        assert result == {'sum_value': 6, 'count': 3}

    def test_with_multiple_filters_and_select(self) -> None:
        """
        Test transforming using multiple filters and a select sequence.

        Notes
        -----
        Filters twice before selecting fields.
        """
        data = [
            {'name': 'John', 'age': 30, 'city': 'New York'},
            {'name': 'Jane', 'age': 25, 'city': 'Newark'},
            {'name': 'Bob', 'age': 35, 'city': 'Boston'},
        ]
        result = transform(
            data,
            {
                'filter': [
                    {'field': 'age', 'op': 'gte', 'value': 26},
                    {
                        'field': 'city',
                        'op': (
                            lambda value, prefix: str(value).startswith(prefix)
                        ),
                        'value': 'New',
                    },
                ],
                'select': [{'fields': ['name']}],
            },
        )
        assert result == [{'name': 'John'}]

    def test_with_select(self) -> None:
        """
        Test transforming using a select operation.

        Notes
        -----
        Keeps only ``name`` and ``age`` fields.
        """
        data = [{'name': 'John', 'age': 30, 'city': 'NYC'}]
        result = transform(data, {'select': ['name', 'age']})
        assert isinstance(result, list)
        assert len(result) == 1
        assert set(result[0].keys()) == {'name', 'age'}

    def test_with_sort(self) -> None:
        """
        Test transforming using a sort operation.

        Notes
        -----
        Sorts by ``age`` ascending.
        """
        data = [
            {'name': 'John', 'age': 30},
            {'name': 'Jane', 'age': 25},
        ]
        result = transform(data, {'sort': {'field': 'age'}})
        assert isinstance(result, list)
        assert len(result) == 2
        assert result[0]['age'] == 25
