"""Tests for result.evaluate() - 3-layer evaluation."""
from __future__ import annotations

import pytest

from kanoniv.source import Source
from kanoniv.spec import Spec

try:
    from kanoniv._native import reconcile_local

    HAS_NATIVE = True
except ImportError:
    HAS_NATIVE = False

pytestmark = pytest.mark.skipif(not HAS_NATIVE, reason="Native extension not built")

# --- Spec with rules-based matching on email ---
SPEC_YAML = """\
api_version: kanoniv/v2
identity_version: test_eval_v1
entity:
  name: customer
sources:
  - name: crm
    system: csv
    table: crm_export
    id: id
    attributes:
      email: email
      name: name
      phone: phone
  - name: orders
    system: csv
    table: order_export
    id: id
    attributes:
      email: email
      name: name
      phone: phone
blocking:
  strategy: composite
  keys:
    - [email]
    - [phone]
rules:
  - name: email_exact
    type: exact
    field: email
    weight: 1.0
  - name: phone_exact
    type: exact
    field: phone
    weight: 1.0
decision:
  thresholds:
    match: 0.8
    review: 0.5
  conflict_strategy: prefer_high_confidence
"""


@pytest.fixture
def perfect_csvs(tmp_path):
    """Two sources where email matching produces perfect results."""
    crm = tmp_path / "crm.csv"
    crm.write_text(
        "id,email,name,phone\n"
        "c1,alice@example.com,Alice,111\n"
        "c2,bob@example.com,Bob,222\n"
    )
    orders = tmp_path / "orders.csv"
    orders.write_text(
        "id,email,name,phone\n"
        "o1,alice@example.com,Alice S,111\n"
        "o2,bob@example.com,Bob J,222\n"
    )
    return str(crm), str(orders)


@pytest.fixture
def overmerge_csvs(tmp_path):
    """Sources where phone matching causes an incorrect merge (false positive)."""
    crm = tmp_path / "crm.csv"
    crm.write_text(
        "id,email,name,phone\n"
        "c1,alice@example.com,Alice,111\n"
        "c2,bob@example.com,Bob,222\n"
        "c3,carol@example.com,Carol,333\n"
    )
    orders = tmp_path / "orders.csv"
    orders.write_text(
        "id,email,name,phone\n"
        "o1,alice@example.com,Alice S,111\n"
        "o2,dave@example.com,Dave,222\n"
    )
    return str(crm), str(orders)


@pytest.fixture
def undermerge_csvs(tmp_path):
    """Sources where records should merge but don't (false negatives)."""
    crm = tmp_path / "crm.csv"
    crm.write_text(
        "id,email,name,phone\n"
        "c1,alice@example.com,Alice,111\n"
        "c2,bob@example.com,Bob,222\n"
    )
    orders = tmp_path / "orders.csv"
    orders.write_text(
        "id,email,name,phone\n"
        # Same alice by email, but different phone - should still merge on email
        "o1,alice@example.com,Alice S,999\n"
        # Bob uses a different email - won't merge
        "o2,robert@example.com,Bob J,222\n"
    )
    return str(crm), str(orders)


class TestEvaluateStructural:
    """Layer 1+2: structural + stability metrics (no ground truth)."""

    def test_structural_metrics_populated(self, perfect_csvs):
        from kanoniv.reconcile import reconcile

        crm_path, orders_path = perfect_csvs
        crm = Source.from_csv("crm", crm_path, primary_key="id")
        orders = Source.from_csv("orders", orders_path, primary_key="id")
        spec = Spec.from_string(SPEC_YAML)

        result = reconcile([crm, orders], spec)
        metrics = result.evaluate()

        # L1: structural
        assert metrics.total_records == 4
        assert metrics.total_clusters >= 1
        assert 0.0 <= metrics.merge_rate <= 1.0
        assert metrics.singletons >= 0
        assert 0.0 <= metrics.singletons_pct <= 1.0
        assert metrics.largest_cluster >= 1
        assert isinstance(metrics.cluster_distribution, dict)
        assert sum(metrics.cluster_distribution.values()) == metrics.total_clusters

        # L2: stability
        assert metrics.pairs_evaluated >= 0
        assert isinstance(metrics.decisions, dict)
        assert isinstance(metrics.field_stats, list)
        assert metrics.blocking_groups >= 0

        # L3: ground truth should be None
        assert metrics.precision is None
        assert metrics.recall is None
        assert metrics.f1 is None
        assert metrics.true_positives is None

    def test_structural_summary_no_gt_section(self, perfect_csvs):
        from kanoniv.reconcile import reconcile

        crm_path, orders_path = perfect_csvs
        crm = Source.from_csv("crm", crm_path, primary_key="id")
        orders = Source.from_csv("orders", orders_path, primary_key="id")
        spec = Spec.from_string(SPEC_YAML)

        result = reconcile([crm, orders], spec)
        metrics = result.evaluate()
        s = metrics.summary()

        assert "Structural" in s
        assert "Stability" in s
        assert "Ground Truth" not in s

    def test_structural_repr_no_gt(self, perfect_csvs):
        from kanoniv.reconcile import reconcile

        crm_path, orders_path = perfect_csvs
        crm = Source.from_csv("crm", crm_path, primary_key="id")
        orders = Source.from_csv("orders", orders_path, primary_key="id")
        spec = Spec.from_string(SPEC_YAML)

        result = reconcile([crm, orders], spec)
        metrics = result.evaluate()
        r = repr(metrics)

        assert "clusters=" in r
        assert "merge_rate=" in r
        assert "precision=" not in r


class TestEvaluateGroundTruth:
    """Layer 3: ground truth P/R/F1."""

    def test_perfect_prediction_dict_format(self, perfect_csvs):
        from kanoniv.reconcile import reconcile

        crm_path, orders_path = perfect_csvs
        crm = Source.from_csv("crm", crm_path, primary_key="id")
        orders = Source.from_csv("orders", orders_path, primary_key="id")
        spec = Spec.from_string(SPEC_YAML)

        result = reconcile([crm, orders], spec)

        gt = {
            "alice": [("crm", "c1"), ("orders", "o1")],
            "bob": [("crm", "c2"), ("orders", "o2")],
        }

        metrics = result.evaluate(ground_truth=gt)

        assert metrics.precision == 1.0
        assert metrics.recall == 1.0
        assert metrics.f1 == 1.0
        assert metrics.true_positives == 2
        assert metrics.false_positives == 0
        assert metrics.false_negatives == 0

        # L1+L2 should also be populated
        assert metrics.total_records == 4
        assert metrics.total_clusters >= 1

    def test_perfect_prediction_dataframe_format(self, perfect_csvs):
        pd = pytest.importorskip("pandas")
        from kanoniv.reconcile import reconcile

        crm_path, orders_path = perfect_csvs
        crm = Source.from_csv("crm", crm_path, primary_key="id")
        orders = Source.from_csv("orders", orders_path, primary_key="id")
        spec = Spec.from_string(SPEC_YAML)

        result = reconcile([crm, orders], spec)

        gt_df = pd.DataFrame([
            {"record_id": "c1", "source_name": "crm", "true_entity_id": "alice"},
            {"record_id": "o1", "source_name": "orders", "true_entity_id": "alice"},
            {"record_id": "c2", "source_name": "crm", "true_entity_id": "bob"},
            {"record_id": "o2", "source_name": "orders", "true_entity_id": "bob"},
        ])

        metrics = result.evaluate(ground_truth=gt_df)

        assert metrics.precision == 1.0
        assert metrics.recall == 1.0
        assert metrics.f1 == 1.0

    def test_false_positives_over_merging(self, overmerge_csvs):
        from kanoniv.reconcile import reconcile

        crm_path, orders_path = overmerge_csvs
        crm = Source.from_csv("crm", crm_path, primary_key="id")
        orders = Source.from_csv("orders", orders_path, primary_key="id")
        spec = Spec.from_string(SPEC_YAML)

        result = reconcile([crm, orders], spec)

        gt = {
            "alice": [("crm", "c1"), ("orders", "o1")],
        }

        metrics = result.evaluate(ground_truth=gt)

        assert metrics.true_positives >= 1
        assert metrics.precision <= 1.0

    def test_false_negatives_under_merging(self, undermerge_csvs):
        from kanoniv.reconcile import reconcile

        crm_path, orders_path = undermerge_csvs
        crm = Source.from_csv("crm", crm_path, primary_key="id")
        orders = Source.from_csv("orders", orders_path, primary_key="id")
        spec = Spec.from_string(SPEC_YAML)

        result = reconcile([crm, orders], spec)

        gt = {
            "alice": [("crm", "c1"), ("orders", "o1")],
            "bob": [("crm", "c2"), ("orders", "o2")],
        }

        metrics = result.evaluate(ground_truth=gt)

        assert metrics.false_negatives >= 1
        assert metrics.recall < 1.0

    def test_gt_summary_includes_all_sections(self, perfect_csvs):
        from kanoniv.reconcile import reconcile

        crm_path, orders_path = perfect_csvs
        crm = Source.from_csv("crm", crm_path, primary_key="id")
        orders = Source.from_csv("orders", orders_path, primary_key="id")
        spec = Spec.from_string(SPEC_YAML)

        result = reconcile([crm, orders], spec)
        gt = {
            "alice": [("crm", "c1"), ("orders", "o1")],
            "bob": [("crm", "c2"), ("orders", "o2")],
        }
        metrics = result.evaluate(ground_truth=gt)
        s = metrics.summary()

        assert "Structural" in s
        assert "Stability" in s
        assert "Ground Truth" in s
        assert "Precision" in s
        assert "1.0000" in s

    def test_gt_repr_shows_precision(self, perfect_csvs):
        from kanoniv.reconcile import reconcile

        crm_path, orders_path = perfect_csvs
        crm = Source.from_csv("crm", crm_path, primary_key="id")
        orders = Source.from_csv("orders", orders_path, primary_key="id")
        spec = Spec.from_string(SPEC_YAML)

        result = reconcile([crm, orders], spec)
        gt = {
            "alice": [("crm", "c1"), ("orders", "o1")],
            "bob": [("crm", "c2"), ("orders", "o2")],
        }
        metrics = result.evaluate(ground_truth=gt)

        assert "precision=1.0000" in repr(metrics)
        assert "recall=1.0000" in repr(metrics)


class TestInternalHelpers:
    def test_pairs_from_clusters(self):
        from kanoniv.evaluate import _pairs_from_clusters

        clusters = [
            {("a", "1"), ("b", "2"), ("c", "3")},
            {("d", "4"), ("e", "5")},
        ]
        pairs = _pairs_from_clusters(clusters)
        # First cluster: 3 pairs (3 choose 2), second: 1 pair
        assert len(pairs) == 4

    def test_pairs_from_single_member_clusters(self):
        from kanoniv.evaluate import _pairs_from_clusters

        clusters = [{("a", "1")}]
        pairs = _pairs_from_clusters(clusters)
        assert len(pairs) == 0

    def test_parse_ground_truth_invalid_type(self):
        from kanoniv.evaluate import _parse_ground_truth

        with pytest.raises(TypeError, match="dict or pandas DataFrame"):
            _parse_ground_truth("invalid")
