"""Unit-тесты для parity-harness'а самого по себе (tests/support/parity.py).

Проверяет три сценария из issue #2304:
- legacy/kit выводы идентичны → assert_parity проходит без исключения;
- выводы различаются → ParityMismatchError с информативным диффом (не просто "not equal");
- tolerance-опция гасит незначащие float-расхождения (напр. latency_ms).

Также покрывает ignore_fields (второй способ игнорировать недетерминированные поля)
и кейс, ради которого harness вообще нужен: dataclass-инстансы РАЗНЫХ классов
(имитация legacy vs kit module) с одинаковыми полями должны сравниваться
структурно, а не через identity классов.
"""

from __future__ import annotations

from dataclasses import dataclass

import pytest

from tests.support.parity import ParityMismatchError, assert_parity, compare_outputs


def test_assert_parity_passes_when_outputs_identical() -> None:
    def legacy_fn(x: int) -> dict[str, int]:
        return {"value": x * 2}

    def kit_fn(x: int) -> dict[str, int]:
        return {"value": x * 2}

    assert_parity(legacy_fn, kit_fn, fixtures=[1, 2, 3])


def test_assert_parity_raises_with_informative_diff_when_outputs_differ() -> None:
    def legacy_fn(x: int) -> dict[str, int]:
        return {"value": x, "count": 10}

    def kit_fn(x: int) -> dict[str, int]:
        return {"value": x, "count": 999}  # намеренное расхождение

    with pytest.raises(ParityMismatchError) as exc_info:
        assert_parity(legacy_fn, kit_fn, fixtures=[1])

    message = str(exc_info.value)
    # Диагностика должна называть КОНКРЕТНОЕ поле и оба значения, не просто "not equal".
    assert "$.count" in message
    assert "10" in message
    assert "999" in message
    # value совпало у обеих функций → не должно попасть в список различий.
    assert "$.value" not in message


def test_assert_parity_tolerance_ignores_small_float_drift() -> None:
    def legacy_fn(x: int) -> dict[str, float]:
        return {"latency_ms": 100.001, "score": 0.5}

    def kit_fn(x: int) -> dict[str, float]:
        return {"latency_ms": 100.004, "score": 0.5}

    # Без tolerance — расхождение 0.003 ловится.
    with pytest.raises(ParityMismatchError):
        assert_parity(legacy_fn, kit_fn, fixtures=[1])

    # С tolerance >= drift — проходит.
    assert_parity(legacy_fn, kit_fn, fixtures=[1], tolerance=0.01)


def test_ignore_fields_skips_named_field_entirely() -> None:
    def legacy_fn(x: int) -> dict[str, object]:
        return {"fetched_at": "2026-01-01T00:00:00Z", "value": x}

    def kit_fn(x: int) -> dict[str, object]:
        return {"fetched_at": "2026-07-03T12:00:00Z", "value": x}  # timestamp всегда разный

    with pytest.raises(ParityMismatchError):
        assert_parity(legacy_fn, kit_fn, fixtures=[1])

    assert_parity(legacy_fn, kit_fn, fixtures=[1], ignore_fields={"fetched_at"})


def test_dataclass_instances_of_different_classes_compared_structurally() -> None:
    # Имитация legacy vs kit: одинаковые поля, РАЗНЫЕ классы (разные модули).
    @dataclass
    class LegacyResult:
        item_id: str
        price: int

    @dataclass
    class KitResult:
        item_id: str
        price: int

    legacy = LegacyResult(item_id="42", price=100)
    kit = KitResult(item_id="42", price=100)

    # Прямое == было бы False (dataclass __eq__ проверяет class identity первым).
    assert legacy != kit
    # compare_outputs сравнивает по полям, а не по классу → различий нет.
    assert compare_outputs(legacy, kit) == []


def test_bool_vs_int_type_mismatch_is_not_silently_equal() -> None:
    # Python bool — подкласс int: `True == 1` и `False == 0` дают True при обычном
    # `==`. Миграция, случайно превратившая bool-поле в сырой 0/1, должна ловиться.
    def legacy_fn(x: int) -> dict[str, object]:
        return {"mortgage_available": True}

    def kit_fn(x: int) -> dict[str, object]:
        return {"mortgage_available": 1}  # намеренно int вместо bool

    with pytest.raises(ParityMismatchError) as exc_info:
        assert_parity(legacy_fn, kit_fn, fixtures=[1])

    message = str(exc_info.value)
    assert "mortgage_available" in message
    assert "bool" in message

    # Обратный случай (False vs 0) и совпадающие типы (bool vs bool) — симметрично.
    assert compare_outputs({"v": False}, {"v": 0}) != []
    assert compare_outputs({"v": True}, {"v": True}) == []


def test_dataclass_field_mismatch_reported_by_name() -> None:
    @dataclass
    class LegacyResult:
        item_id: str
        price: int

    @dataclass
    class KitResult:
        item_id: str
        price: int

    legacy = LegacyResult(item_id="42", price=100)
    kit = KitResult(item_id="42", price=200)

    diffs = compare_outputs(legacy, kit)
    assert len(diffs) == 1
    assert "price" in diffs[0]
    assert "100" in diffs[0]
    assert "200" in diffs[0]
