import numpy as np
import types
import sys
import types as _types

# Provide a minimal duckdb stub when the real package is not available in the test env
try:
    import duckdb as _duckdb
except Exception:
    import pandas as _pd

    class FakeDuckDBConnection:
        def __init__(self):
            # storage for mp_votes rows: list of tuples matching _make_mp_votes_db
            self._mp_votes = []

        def execute(self, sql, params=None):
            s = sql.strip().lower()
            # simple create/select handling: return empty results for schema queries
            if s.startswith("create table") or s.startswith(
                "select distinct window_id"
            ):
                return _types.SimpleNamespace(fetchall=lambda: [])

            # compute_party_discipline query detection
            if (
                "from rice_per_motion" in s
                or "select\n                party,\n                count(distinct motion_id) as n_motions"
                in sql
            ):
                # params: [start_date, end_date]
                start_date, end_date = params or [None, None]
                # filter rows by mp_name like '%,%' and date range and vote in ('voor','tegen')
                rows = [r for r in self._mp_votes if ("," in (r[2] or ""))]
                if start_date:
                    rows = [r for r in rows if r[5] >= start_date and r[5] <= end_date]
                rows = [r for r in rows if (r[4] in ("voor", "tegen"))]

                # build counts per motion_id, party, vote
                from collections import defaultdict

                counts = defaultdict(lambda: defaultdict(lambda: defaultdict(int)))
                motions = set()
                for _id, motion_id, mp_name, party, vote, date, created_at in rows:
                    counts[motion_id][party][vote] += 1
                    motions.add((motion_id, party))

                # compute rice per (motion, party)
                rice_vals = defaultdict(list)  # party -> list of rice per motion
                motion_part_set = set()
                for motion_id, party_counts in counts.items():
                    for party, vc in party_counts.items():
                        total = sum(vc.values())
                        if total == 0:
                            continue
                        # majority vote: vote with max count, tie-breaker by vote asc
                        maj_vote = sorted(vc.items(), key=lambda kv: (-kv[1], kv[0]))[
                            0
                        ][0]
                        same = vc.get(maj_vote, 0)
                        rice = same / float(total)
                        rice_vals[party].append((motion_id, rice))
                        motion_part_set.add((motion_id, party))

                # aggregate per party
                import pandas as pd

                rows_out = []
                for party, lst in rice_vals.items():
                    n_motions = len({m for m, _ in lst})
                    avg_rice = sum(r for _, r in lst) / n_motions if n_motions else 0.0
                    rows_out.append(
                        {"party": party, "n_motions": n_motions, "discipline": avg_rice}
                    )

                df = pd.DataFrame(rows_out)
                return _types.SimpleNamespace(fetchdf=lambda: df)

            # default fallback
            return _types.SimpleNamespace(fetchall=lambda: [])

        def executemany(self, sql, rows):
            s = sql.strip().lower()
            if s.startswith("insert into mp_votes"):
                for r in rows:
                    self._mp_votes.append(r)

        def close(self):
            return None

    _fake_duckdb = _types.ModuleType("duckdb")
    _fake_duckdb.connect = lambda *a, **kw: FakeDuckDBConnection()
    sys.modules["duckdb"] = _fake_duckdb
    _duckdb = _fake_duckdb

# Provide a minimal plotly.express stub so explorer imports in tests without requiring plotly
try:
    import plotly.express as px  # type: ignore
except Exception:
    _px = types.ModuleType("plotly.express")
    _px.scatter = lambda *a, **kw: None
    _px.line = lambda *a, **kw: None
    # Ensure top-level 'plotly' package exists and exposes express
    _plotly_pkg = types.ModuleType("plotly")
    _plotly_pkg.express = _px
    sys.modules["plotly"] = _plotly_pkg
    sys.modules["plotly.express"] = _px
    px = _px
    # stub plotly.graph_objects too
    _go = types.ModuleType("plotly.graph_objects")
    _go.Figure = lambda *a, **kw: None
    sys.modules["plotly.graph_objects"] = _go

# Provide a minimal streamlit stub so explorer imports succeed in the test env
try:
    import streamlit as _st  # noqa: F401
except Exception:
    _st_stub = types.ModuleType("streamlit")
    _st_stub.cache_data = lambda **kw: lambda f: f
    _st_stub.plotly_chart = lambda *a, **kw: None
    _st_stub.markdown = lambda *a, **kw: None
    _st_stub.caption = lambda *a, **kw: None
    _st_stub.error = lambda *a, **kw: None
    _st_stub.warning = lambda *a, **kw: None
    _st_stub.info = lambda *a, **kw: None
    _st_stub.write = lambda *a, **kw: None
    sys.modules["streamlit"] = _st_stub

import pytest


# ---------------------------------------------------------------------------
# Helpers shared by orientation tests
# ---------------------------------------------------------------------------


def _make_fake_traj(aligned):
    fake = types.SimpleNamespace()
    fake._load_window_ids = lambda db: list(aligned.keys())
    fake._load_mp_vectors_for_window = lambda db, w: aligned.get(w, {})
    fake._procrustes_align_windows = lambda x: aligned
    return fake


def test_compute_2d_axes_pca_synthetic(monkeypatch):
    """Synthetic test for compute_2d_axes using patched alignment helper."""

    # Create a fake trajectory module with required helpers
    fake_traj = types.SimpleNamespace()

    # _load_window_ids should return ordered windows
    fake_traj._load_window_ids = lambda db: ["w1", "w2"]

    # Provide aligned vectors directly
    aligned = {
        "w1": {"Alice": np.array([1.0, 0.0, 0.0]), "Bob": np.array([0.0, 1.0, 0.0])},
        "w2": {"Alice": np.array([0.8, 0.2, 0.0]), "Bob": np.array([0.1, 0.9, 0.0])},
    }

    # _load_mp_vectors_for_window returns the pre-aligned vectors (needed for padding step)
    fake_traj._load_mp_vectors_for_window = lambda db, w: aligned.get(w, {})

    fake_traj._procrustes_align_windows = lambda x: aligned

    # Insert fake module into sys.modules for import by analysis.political_axis
    monkeypatch.setitem(sys.modules, "analysis.trajectory", fake_traj)

    # Now import the function under test
    from analysis.political_axis import compute_2d_axes

    positions_by_window, axis_def = compute_2d_axes(
        db_path="dummy", window_ids=["w1", "w2"], method="pca"
    )

    assert "w1" in positions_by_window and "w2" in positions_by_window
    for wid in ("w1", "w2"):
        for name, coord in positions_by_window[wid].items():
            assert len(coord) == 2
            assert np.isfinite(coord[0]) and np.isfinite(coord[1])

    assert axis_def.get("method") == "pca"


def test_per_window_y_orientation(monkeypatch):
    """Per-window Y correction must ensure prog_avg_y > cons_avg_y in every window.

    We construct two windows:
    - w_good: progressive MPs at +Y, conservative MPs at -Y (already correct)
    - w_bad:  conservative MPs at +Y, progressive MPs at -Y (inverted)

    We weight w_good with many more MPs so the GLOBAL centroid check passes
    without noticing the per-window inversion. The per-window correction must
    then flip w_bad so both windows end up with prog_avg_y > cons_avg_y.
    """

    # Helpers to make slightly varied vectors
    def pv(base):
        return np.array(base, dtype=float)

    # w_good: large left/right spread on dim-0, prog up (+Y), cons down (-Y)
    w_good = {
        # right / conservative
        "Wilders, G.": pv([-3.0, -1.0, 0.0]),
        "Rutte, M.": pv([-3.0, -0.9, 0.0]),
        "van der Staaij, K.": pv([-2.9, -0.95, 0.0]),
        "Omtzigt, P.": pv([-2.8, -0.85, 0.0]),
        # left / progressive
        "Marijnissen, L.": pv([3.0, 1.0, 0.0]),
        "Klever, A.": pv([3.0, 0.9, 0.0]),
        "Bromet, L.": pv([2.9, 0.95, 0.0]),
        "Nijboer, H.": pv([2.8, 0.85, 0.0]),
    }

    # w_bad: same left/right structure but Y is inverted relative to w_good
    # (conservative at +Y, progressive at -Y)
    w_bad = {
        "Wilders, G.": pv([-3.0, 1.0, 0.0]),  # cons at +Y
        "Rutte, M.": pv([-3.0, 0.9, 0.0]),
        "van der Staaij, K.": pv([-2.9, 0.95, 0.0]),
        "Omtzigt, P.": pv([-2.8, 0.85, 0.0]),
        "Marijnissen, L.": pv([3.0, -1.0, 0.0]),  # prog at -Y
        "Klever, A.": pv([3.0, -0.9, 0.0]),
        "Bromet, L.": pv([2.9, -0.95, 0.0]),
        "Nijboer, H.": pv([2.8, -0.85, 0.0]),
    }

    aligned = {"w_good": w_good, "w_bad": w_bad}

    mp_metadata = [
        ("Wilders, G.", "PVV"),
        ("Rutte, M.", "VVD"),
        ("van der Staaij, K.", "SGP"),
        ("Omtzigt, P.", "Nieuw Sociaal Contract"),
        ("Marijnissen, L.", "SP"),
        ("Klever, A.", "GroenLinks-PvdA"),
        ("Bromet, L.", "GroenLinks-PvdA"),
        ("Nijboer, H.", "SP"),
    ]

    fake_traj = _make_fake_traj(aligned)
    monkeypatch.setitem(sys.modules, "analysis.trajectory", fake_traj)

    import types as _types

    fake_conn = _types.SimpleNamespace(
        execute=lambda q: _types.SimpleNamespace(fetchall=lambda: mp_metadata),
        close=lambda: None,
    )
    import duckdb as _duckdb

    monkeypatch.setattr(_duckdb, "connect", lambda db_path, **kw: fake_conn)

    import importlib, analysis.political_axis as _ax

    importlib.reload(_ax)
    from analysis.political_axis import compute_2d_axes

    positions_by_window, axis_def = compute_2d_axes(
        db_path="dummy", window_ids=["w_good", "w_bad"], method="pca"
    )

    prog_mps = {"Marijnissen, L.", "Klever, A.", "Bromet, L.", "Nijboer, H."}
    cons_mps = {"Wilders, G.", "Rutte, M.", "van der Staaij, K.", "Omtzigt, P."}

    for wid in ("w_good", "w_bad"):
        pos = positions_by_window[wid]
        prog_y = np.mean([pos[mp][1] for mp in prog_mps if mp in pos])
        cons_y = np.mean([pos[mp][1] for mp in cons_mps if mp in pos])
        assert prog_y > cons_y, (
            f"Window '{wid}': expected prog_avg_y ({prog_y:.3f}) > cons_avg_y ({cons_y:.3f})"
        )


def test_pca_axis_orientation(monkeypatch):
    """PCA axes must be oriented so right parties score higher on X and
    progressive parties score higher on Y than their respective opposites.

    We construct a minimal vote-matrix world where:
    - Right MPs (PVV, VVD members) cluster in one direction on dim-0.
    - Left MPs (SP, GroenLinks-PvdA members) cluster in the opposite direction.
    - Progressive MPs cluster on dim-1; conservative MPs on the opposite side.

    The orientation logic in compute_2d_axes should flip axis signs so that
    right_x > left_x and prog_y > cons_y regardless of the raw SVD sign.
    """
    # Build vectors so that right parties are at +1 on dim-0 and
    # progressive parties are at +1 on dim-1.
    # We deliberately negate them to test that auto-orient flips them back.
    # Right/left use magnitude 3, prog/cons use magnitude 1 so that dim-0
    # dominates PCA variance — ensuring PC1 = left-right axis, PC2 = prog-cons.
    right_vec = np.array([-3.0, 0.0, 0.0])  # intentionally negative on dim-0
    left_vec = np.array([3.0, 0.0, 0.0])  # intentionally positive on dim-0
    prog_vec = np.array([0.0, -1.0, 0.0])  # intentionally negative on dim-1
    cons_vec = np.array([0.0, 1.0, 0.0])  # intentionally positive on dim-1

    aligned = {
        "w1": {
            # Right-leaning MPs
            "Wilders, G.": right_vec,
            "Rutte, M.": right_vec + np.array([0.0, 0.0, 0.05]),
            # Left-leaning MPs
            "Marijnissen, L.": left_vec,
            "Klever, A.": left_vec + np.array([0.0, 0.0, 0.05]),
            # Progressive MPs
            "Bromet, L.": prog_vec,
            "Nijboer, H.": prog_vec + np.array([0.0, 0.0, -0.05]),
            # Conservative MPs
            "Segers, G.": cons_vec,
            "Omtzigt, P.": cons_vec + np.array([0.0, 0.0, -0.05]),
        }
    }

    # mp_metadata rows used by the orientation code (party affiliation)
    mp_metadata = [
        ("Wilders, G.", "PVV"),
        ("Rutte, M.", "VVD"),
        ("Marijnissen, L.", "SP"),
        ("Klever, A.", "GroenLinks-PvdA"),
        ("Bromet, L.", "GroenLinks-PvdA"),
        ("Nijboer, H.", "SP"),
        ("Segers, G.", "CDA"),
        ("Omtzigt, P.", "Nieuw Sociaal Contract"),
    ]

    fake_traj = _make_fake_traj(aligned)
    monkeypatch.setitem(sys.modules, "analysis.trajectory", fake_traj)

    # Patch duckdb so the orientation helper can fetch mp_metadata
    import types as _types

    fake_conn = _types.SimpleNamespace(
        execute=lambda q: _types.SimpleNamespace(fetchall=lambda: mp_metadata),
        close=lambda: None,
    )
    import duckdb as _duckdb

    monkeypatch.setattr(_duckdb, "connect", lambda db_path, **kw: fake_conn)

    # Need to reload the module so monkeypatched sys.modules takes effect
    import importlib, analysis.political_axis as _ax

    importlib.reload(_ax)
    from analysis.political_axis import compute_2d_axes

    positions_by_window, axis_def = compute_2d_axes(
        db_path="dummy", window_ids=["w1"], method="pca"
    )

    pos = positions_by_window["w1"]

    # X-axis: right parties should score higher than left parties
    right_x = np.mean([pos["Wilders, G."][0], pos["Rutte, M."][0]])
    left_x = np.mean([pos["Marijnissen, L."][0], pos["Klever, A."][0]])
    assert right_x > left_x, (
        f"Expected right parties (x={right_x:.3f}) > left parties (x={left_x:.3f}) on X-axis"
    )

    # Y-axis: progressive parties should score higher than conservative parties
    prog_y = np.mean([pos["Bromet, L."][1], pos["Nijboer, H."][1]])
    cons_y = np.mean([pos["Segers, G."][1], pos["Omtzigt, P."][1]])
    assert prog_y > cons_y, (
        f"Expected progressive parties (y={prog_y:.3f}) > conservative parties (y={cons_y:.3f}) on Y-axis"
    )


# ---------------------------------------------------------------------------
# Tests for compute_party_discipline
# ---------------------------------------------------------------------------


def _make_mp_votes_db():
    """Create an in-memory DuckDB with mp_votes fixture data.

    6 motions, 2 parties (SP, VVD), each with 4 MPs.
    SP is perfectly disciplined (all 4 vote the same each time).
    VVD has 1 dissident on 2 of 6 motions → Rice index = (4+4+4+4+3+3)/6/4 ≈ 0.917.
    Dates span 2023-01-01 to 2023-12-31.
    """
    import duckdb

    conn = duckdb.connect(":memory:")
    conn.execute("""
        CREATE TABLE mp_votes (
            id INTEGER,
            motion_id VARCHAR,
            mp_name VARCHAR,
            party VARCHAR,
            vote VARCHAR,
            date DATE,
            created_at TIMESTAMP
        )
    """)
    rows = []
    dates = [
        "2023-01-10",
        "2023-03-15",
        "2023-05-20",
        "2023-07-25",
        "2023-09-30",
        "2023-11-05",
    ]
    sp_mps = ["Janssen, A.", "Pietersen, B.", "Willemsen, C.", "Hendriksen, D."]
    vvd_mps = ["Adams, E.", "Bakker, F.", "Claassen, G.", "Dekker, H."]
    for i, date in enumerate(dates, start=1):
        m_id = f"M{i:03d}"
        for mp in sp_mps:
            rows.append((i * 10 + 1, m_id, mp, "SP", "voor", date, "2023-01-01"))
        if i <= 4:
            for mp in vvd_mps:
                rows.append((i * 10 + 2, m_id, mp, "VVD", "voor", date, "2023-01-01"))
        else:
            for mp in vvd_mps[:3]:
                rows.append((i * 10 + 2, m_id, mp, "VVD", "voor", date, "2023-01-01"))
            rows.append(
                (i * 10 + 3, m_id, vvd_mps[3], "VVD", "tegen", date, "2023-01-01")
            )
    conn.executemany("INSERT INTO mp_votes VALUES (?, ?, ?, ?, ?, ?, ?)", rows)
    return conn


def test_compute_party_discipline_basic(monkeypatch):
    """compute_party_discipline returns correct Rice index for fixture data."""
    import duckdb as _duckdb

    fixture_conn = _make_mp_votes_db()

    monkeypatch.setattr(_duckdb, "connect", lambda path, **kw: fixture_conn)

    import importlib
    import sys

    if "streamlit" not in sys.modules:
        import types

        st_stub = types.ModuleType("streamlit")
        st_stub.cache_data = lambda **kw: lambda f: f
        sys.modules["streamlit"] = st_stub

    import explorer as _explorer

    importlib.reload(_explorer)

    df = _explorer.compute_party_discipline(
        db_path="dummy",
        start_date="2023-01-01",
        end_date="2023-12-31",
    )

    assert not df.empty
    assert set(df.columns) >= {"party", "n_motions", "discipline"}

    sp_row = df[df["party"] == "SP"].iloc[0]
    vvd_row = df[df["party"] == "VVD"].iloc[0]

    assert sp_row["n_motions"] == 6
    assert sp_row["discipline"] == pytest.approx(1.0, abs=1e-6)

    assert vvd_row["n_motions"] == 6
    expected_vvd = (4 * 1.0 + 2 * 0.75) / 6
    assert vvd_row["discipline"] == pytest.approx(expected_vvd, abs=1e-4)

    assert (df["discipline"] >= 0).all() and (df["discipline"] <= 1).all()


def test_compute_party_discipline_empty_range(monkeypatch):
    """Returns empty DataFrame when no motions fall in the date range."""
    import duckdb as _duckdb

    fixture_conn = _make_mp_votes_db()
    monkeypatch.setattr(_duckdb, "connect", lambda path, **kw: fixture_conn)

    import importlib, sys

    if "streamlit" not in sys.modules:
        import types

        st_stub = types.ModuleType("streamlit")
        st_stub.cache_data = lambda **kw: lambda f: f
        sys.modules["streamlit"] = st_stub

    import explorer as _explorer

    importlib.reload(_explorer)

    df = _explorer.compute_party_discipline(
        db_path="dummy",
        start_date="2000-01-01",
        end_date="2000-12-31",
    )

    assert df.empty


# ---------------------------------------------------------------------------
# Tests for analysis.axis_classifier
# ---------------------------------------------------------------------------

import importlib


def _fresh_classifier(monkeypatch):
    """Import axis_classifier with cleared module-level caches."""
    import analysis.axis_classifier as _cls

    monkeypatch.setattr(_cls, "_ideology_cache", None)
    monkeypatch.setattr(_cls, "_coalition_cache", None)
    return _cls


def test_axis_label_left_right(tmp_path, monkeypatch):
    """Positions that closely correlate with left_right scores → label 'Verzorgingsstaat–Marktwerking'."""
    _cls = _fresh_classifier(monkeypatch)

    (tmp_path / "party_ideologies.csv").write_text(
        "party,left_right,progressive\n"
        "VVD,0.65,0.10\n"
        "PvdA,-0.70,0.75\n"
        "SP,-0.90,0.50\n"
        "PVV,0.90,-0.50\n"
        "D66,-0.10,0.85\n"
        "CDA,0.25,-0.45\n"
    )
    (tmp_path / "coalition_membership.csv").write_text("window_id,party\n")

    # X values are the party's left_right scores — perfect correlation
    positions_by_window = {
        "2022": {
            "VVD": (0.65, 0.10),
            "PvdA": (-0.70, 0.20),
            "SP": (-0.90, 0.30),
            "PVV": (0.90, -0.10),
            "D66": (-0.10, 0.40),
            "CDA": (0.25, -0.20),
        }
    }
    axes = {"x_axis": None, "y_axis": None, "method": "pca"}

    result = _cls.classify_axes(positions_by_window, axes, str(tmp_path / "motions.db"))

    assert result["x_label"] == "Verzorgingsstaat–Marktwerking"
    assert result["x_quality"]["2022"] >= 0.65


def test_axis_label_coalition_dominant(tmp_path, monkeypatch):
    """Positions that match coalition pattern but NOT left-right → 'Coalitie–Oppositie'."""
    _cls = _fresh_classifier(monkeypatch)

    (tmp_path / "party_ideologies.csv").write_text(
        "party,left_right,progressive\n"
        "VVD,0.65,0.10\n"
        "PvdA,-0.70,0.75\n"
        "SP,-0.90,0.50\n"
        "PVV,0.90,-0.50\n"
        "D66,-0.10,0.85\n"
        "CDA,0.25,-0.45\n"
    )
    # 2016: Rutte II coalition = VVD + PvdA
    (tmp_path / "coalition_membership.csv").write_text(
        "window_id,party\n2016,VVD\n2016,PvdA\n"
    )

    # Coalition parties (VVD + PvdA) at x ≈ +1, opposition at x ≈ -1.
    # VVD (right) and PvdA (left) are both near +1 → low left_right correlation
    # but high coalition correlation.
    positions_by_window = {
        "2016": {
            "VVD": (0.95, 0.10),
            "PvdA": (0.90, 0.20),
            "SP": (-0.85, 0.30),
            "PVV": (-0.95, -0.10),
            "D66": (-0.80, 0.40),
            "CDA": (-0.75, -0.20),
        }
    }
    axes = {"x_axis": None, "y_axis": None, "method": "pca"}

    result = _cls.classify_axes(positions_by_window, axes, str(tmp_path / "motions.db"))

    assert result["x_label"] == "Coalitie\u2013Oppositie"
    assert "coalitie" in result["x_interpretation"]["2016"].lower()


def test_axis_classifier_missing_csv(tmp_path, monkeypatch):
    """Missing party_ideologies.csv → returns axes dict unchanged, no exception."""
    _cls = _fresh_classifier(monkeypatch)

    # No CSVs written — directory exists but files do not
    positions_by_window = {"2022": {"VVD": (1.0, 0.5), "PvdA": (-1.0, 0.3)}}
    axes = {"x_axis": None, "y_axis": None, "method": "pca"}

    result = _cls.classify_axes(positions_by_window, axes, str(tmp_path / "motions.db"))

    # Must not crash and must return the original axes dict unchanged
    assert result is axes
    assert "x_label" not in result


def test_compute_2d_axes_exposes_global_mean(monkeypatch):
    """axes dict returned by compute_2d_axes must contain 'global_mean'."""
    fake_traj = types.SimpleNamespace()
    fake_traj._load_window_ids = lambda db: ["w1"]
    aligned = {
        "w1": {
            "Alice": np.array([1.0, 0.0, 0.0]),
            "Bob": np.array([-1.0, 0.5, 0.0]),
        }
    }
    fake_traj._load_mp_vectors_for_window = lambda db, w: aligned.get(w, {})
    fake_traj._procrustes_align_windows = lambda x: aligned
    monkeypatch.setitem(sys.modules, "analysis.trajectory", fake_traj)
    # Provide a minimal duckdb stub so importing analysis.political_axis succeeds
    import types as _types

    fake_conn = _types.SimpleNamespace(
        execute=lambda q: _types.SimpleNamespace(fetchall=lambda: []),
        close=lambda: None,
    )
    fake_duckdb = _types.SimpleNamespace(connect=lambda db_path, **kw: fake_conn)
    monkeypatch.setitem(sys.modules, "duckdb", fake_duckdb)

    from analysis.political_axis import compute_2d_axes

    _, axis_def = compute_2d_axes(db_path="dummy", window_ids=["w1"], method="pca")
    assert "global_mean" in axis_def
    assert isinstance(axis_def["global_mean"], np.ndarray)


def test_classify_from_titles_left_right():
    """Titles dominated by left-right keywords -> 'Verzorgingsstaat–Marktwerking'."""
    from analysis.axis_classifier import _classify_from_titles

    titles = [
        "Motie over asielbeleid",
        "Motie over minimumloon verhoging",
        "Motie over vluchtelingen opvang",
        "Motie over belastingverlaging",
        "Motie over bijstandsuitkering",
    ]
    label, confidence = _classify_from_titles(titles)
    assert label == "Verzorgingsstaat–Marktwerking"
    assert confidence >= 0.4


def test_classify_from_titles_progressive():
    """Titles dominated by progressive/conservative keywords -> 'ProgressiefConservatief'."""
    from analysis.axis_classifier import _classify_from_titles

    titles = [
        "Motie over klimaatdoelstellingen",
        "Motie over stikstofbeleid",
        "Motie over duurzame energie",
        "Motie over co2 uitstoot",
        "Motie over energietransitie",
    ]
    label, confidence = _classify_from_titles(titles)
    assert label == "Progressief\u2013Conservatief"
    assert confidence >= 0.4


def test_classify_from_titles_low_confidence():
    """Mixed/irrelevant titles -> None (fallback triggered)."""
    from analysis.axis_classifier import _classify_from_titles

    titles = [
        "Motie over sportsubsidie",
        "Motie over bibliotheekregeling",
        "Motie over verkeersveiligheid",
    ]
    label, confidence = _classify_from_titles(titles)
    assert label is None
    assert confidence < 0.4


def test_axis_swap_when_y_is_left_right():
    """When y_label is economic left-right and x_label is not, positions must be swapped."""
    from explorer import _swap_axes

    positions_by_window = {
        "2023": {
            "VVD": (0.5, 0.8),
            "PvdA": (-0.3, -0.6),
        }
    }
    axis_def = {
        "x_label": "Progressief\u2013Conservatief",
        "y_label": "Verzorgingsstaat–Marktwerking",
        "x_quality": {"2023": 0.7},
        "y_quality": {"2023": 0.8},
        "x_interpretation": {"2023": "prog interpretation"},
        "y_interpretation": {"2023": "economic interpretation"},
        "x_top_motions": {"2023": {"+": [], "-": []}},
        "y_top_motions": {"2023": {"+": [], "-": []}},
        "x_label_confidence": {"2023": 0.5},
        "y_label_confidence": {"2023": 0.7},
    }

    new_pos, new_ax = _swap_axes(positions_by_window, axis_def)

    # Positions swapped: (x, y) → (y, x)
    assert new_pos["2023"]["VVD"] == (0.8, 0.5)
    assert new_pos["2023"]["PvdA"] == (-0.6, -0.3)

    # Labels swapped
    assert new_ax["x_label"] == "Verzorgingsstaat–Marktwerking"
    assert new_ax["y_label"] == "Progressief\u2013Conservatief"

    # Quality swapped
    assert new_ax["x_quality"] == {"2023": 0.8}
    assert new_ax["y_quality"] == {"2023": 0.7}


def test_axis_swap_not_applied_when_x_is_left_right():
    """When x_label is already economic left-right, no swap should occur."""
    from explorer import _should_swap_axes

    axis_def = {
        "x_label": "Verzorgingsstaat–Marktwerking",
        "y_label": "Progressief\u2013Conservatief",
    }
    assert _should_swap_axes(axis_def) is False

    axis_def2 = {
        "x_label": "Verzorgingsstaat–Marktwerking",
        "y_label": "Verzorgingsstaat–Marktwerking",  # both economic — no swap
    }
    assert _should_swap_axes(axis_def2) is False


def test_axis_swap_not_applied_when_neither_axis_is_left_right():
    from explorer import _should_swap_axes

    assert _should_swap_axes({"x_label": "Foo", "y_label": "Bar"}) is False
    assert _should_swap_axes({"x_label": None, "y_label": None}) is False
