"""Tests for sort specification optimizer."""

import pytest

from therismos.sorting import SortCriterion, SortOrder, SortSpec
from therismos.sorting.optimizer import optimize


class TestRemoveNoneOrders:
    """Tests for removing criteria with SortOrder.NONE."""

    def test_remove_single_none_order(self):
        """Test removing a single NONE order criterion."""
        spec = SortSpec(
            [
                SortCriterion("age", SortOrder.ASCENDING),
                SortCriterion("name", SortOrder.NONE),
                SortCriterion("score", SortOrder.DESCENDING),
            ]
        )

        optimized, records = optimize(spec)

        assert len(optimized) == 2
        assert optimized[0].field == "age"
        assert optimized[1].field == "score"
        assert any("NONE" in record.reason for record in records)

    def test_remove_multiple_none_orders(self):
        """Test removing multiple NONE order criteria."""
        spec = SortSpec(
            [
                SortCriterion("a", SortOrder.NONE),
                SortCriterion("b", SortOrder.ASCENDING),
                SortCriterion("c", SortOrder.NONE),
                SortCriterion("d", SortOrder.DESCENDING),
            ]
        )

        optimized, records = optimize(spec)

        assert len(optimized) == 2
        assert optimized[0].field == "b"
        assert optimized[1].field == "d"

    def test_all_none_orders(self):
        """Test spec with all NONE orders becomes empty."""
        spec = SortSpec(
            [
                SortCriterion("a", SortOrder.NONE),
                SortCriterion("b", SortOrder.NONE),
            ]
        )

        optimized, records = optimize(spec)

        assert len(optimized) == 0
        assert len(records) == 1
        assert "2 criteria" in records[0].reason

    def test_no_none_orders(self):
        """Test spec without NONE orders is unchanged."""
        spec = SortSpec(
            [
                SortCriterion("age", SortOrder.ASCENDING),
                SortCriterion("name", SortOrder.DESCENDING),
            ]
        )

        optimized, records = optimize(spec)

        # Should have optimization for redundant criteria check, but not for NONE
        assert len(optimized) == 2
        # No NONE-related records should be present
        none_records = [r for r in records if "NONE" in r.reason]
        assert len(none_records) == 0


class TestRemoveRedundantCriteria:
    """Tests for removing redundant (overridden) criteria."""

    def test_remove_duplicate_field_keep_last(self):
        """Test that only the last occurrence of a field is kept."""
        spec = SortSpec(
            [
                SortCriterion("age", SortOrder.ASCENDING),
                SortCriterion("name", SortOrder.DESCENDING),
                SortCriterion("age", SortOrder.DESCENDING),
            ]
        )

        optimized, records = optimize(spec)

        assert len(optimized) == 2
        assert optimized[0].field == "name"
        assert optimized[1].field == "age"
        assert optimized[1].order == SortOrder.DESCENDING
        assert any("redundant" in record.reason for record in records)

    def test_multiple_duplicates(self):
        """Test removing multiple duplicate fields."""
        spec = SortSpec(
            [
                SortCriterion("a", SortOrder.ASCENDING),
                SortCriterion("b", SortOrder.DESCENDING),
                SortCriterion("a", SortOrder.DESCENDING),
                SortCriterion("b", SortOrder.ASCENDING),
                SortCriterion("c", SortOrder.ASCENDING),
            ]
        )

        optimized, records = optimize(spec)

        assert len(optimized) == 3
        assert optimized[0].field == "a"
        assert optimized[0].order == SortOrder.DESCENDING
        assert optimized[1].field == "b"
        assert optimized[1].order == SortOrder.ASCENDING
        assert optimized[2].field == "c"

    def test_triple_occurrence_keeps_last(self):
        """Test that with three occurrences, only the last is kept."""
        spec = SortSpec(
            [
                SortCriterion("x", SortOrder.ASCENDING),
                SortCriterion("x", SortOrder.DESCENDING),
                SortCriterion("x", SortOrder.NONE),
            ]
        )

        optimized, records = optimize(spec)

        # Optimizer removes NONE first, leaving [ASCENDING, DESCENDING]
        # Then removes redundant, keeping last non-NONE occurrence: [DESCENDING]
        assert len(optimized) == 1
        assert optimized[0].field == "x"
        assert optimized[0].order == SortOrder.DESCENDING

    def test_no_duplicates(self):
        """Test spec without duplicates is unchanged."""
        spec = SortSpec(
            [
                SortCriterion("age", SortOrder.ASCENDING),
                SortCriterion("name", SortOrder.DESCENDING),
                SortCriterion("score", SortOrder.ASCENDING),
            ]
        )

        optimized, records = optimize(spec)

        assert len(optimized) == 3
        # No redundancy-related records should be present
        redundant_records = [r for r in records if "redundant" in r.reason]
        assert len(redundant_records) == 0


class TestCombinedOptimizations:
    """Tests for combined optimization rules."""

    def test_none_and_redundant_together(self):
        """Test removing both NONE orders and redundant criteria."""
        spec = SortSpec(
            [
                SortCriterion("age", SortOrder.ASCENDING),
                SortCriterion("name", SortOrder.NONE),
                SortCriterion("age", SortOrder.DESCENDING),
                SortCriterion("score", SortOrder.ASCENDING),
            ]
        )

        optimized, records = optimize(spec)

        assert len(optimized) == 2
        assert optimized[0].field == "age"
        assert optimized[0].order == SortOrder.DESCENDING
        assert optimized[1].field == "score"

    def test_empty_spec_unchanged(self):
        """Test that empty spec remains empty."""
        spec = SortSpec()

        optimized, records = optimize(spec)

        assert len(optimized) == 0
        assert len(records) == 0

    def test_single_criterion_unchanged(self):
        """Test that single criterion spec is unchanged."""
        spec = SortSpec(
            [
                SortCriterion("age", SortOrder.ASCENDING),
            ]
        )

        optimized, records = optimize(spec)

        assert len(optimized) == 1
        assert optimized[0].field == "age"


class TestOptimizationRecords:
    """Tests for optimization record tracking."""

    def test_records_track_transformations(self):
        """Test that optimization records are properly created."""
        spec = SortSpec(
            [
                SortCriterion("age", SortOrder.ASCENDING),
                SortCriterion("name", SortOrder.NONE),
                SortCriterion("age", SortOrder.DESCENDING),
            ]
        )

        records = []
        optimized, returned_records = optimize(spec, records)

        # Should have records for both NONE removal and redundant removal
        assert len(returned_records) >= 1
        assert records is returned_records

    def test_optimization_record_structure(self):
        """Test that optimization records have correct structure."""
        spec = SortSpec(
            [
                SortCriterion("a", SortOrder.NONE),
                SortCriterion("b", SortOrder.ASCENDING),
            ]
        )

        _, records = optimize(spec)

        assert len(records) > 0
        for record in records:
            assert hasattr(record, "before")
            assert hasattr(record, "after")
            assert hasattr(record, "reason")
            assert isinstance(record.reason, str)

    def test_no_optimization_no_records(self):
        """Test that no records are created when no optimization is needed."""
        spec = SortSpec(
            [
                SortCriterion("age", SortOrder.ASCENDING),
                SortCriterion("name", SortOrder.DESCENDING),
            ]
        )

        _, records = optimize(spec)

        # No optimizations should be applied
        assert len(records) == 0


@pytest.mark.parametrize(
    "spec_criteria,expected_count,description",
    [
        # Simple cases
        (
            [SortCriterion("a", SortOrder.ASCENDING)],
            1,
            "Single criterion unchanged",
        ),
        (
            [],
            0,
            "Empty spec unchanged",
        ),
        # NONE removal cases
        (
            [
                SortCriterion("a", SortOrder.NONE),
                SortCriterion("b", SortOrder.ASCENDING),
            ],
            1,
            "Remove one NONE",
        ),
        # Redundancy cases
        (
            [
                SortCriterion("a", SortOrder.ASCENDING),
                SortCriterion("a", SortOrder.DESCENDING),
            ],
            1,
            "Remove redundant, keep last",
        ),
        # Combined cases
        (
            [
                SortCriterion("a", SortOrder.NONE),
                SortCriterion("b", SortOrder.ASCENDING),
                SortCriterion("b", SortOrder.DESCENDING),
            ],
            1,
            "NONE removal and redundancy removal",
        ),
    ],
)
def test_optimization_parametrized(spec_criteria, expected_count, description):
    """Parametrized test for various optimization scenarios."""
    spec = SortSpec(spec_criteria)
    optimized, _ = optimize(spec)

    assert len(optimized) == expected_count, description
