"""Tests for InsAIts Pattern Miner."""

import json
import os
import pytest
from pathlib import Path

from insa_its.learning.pattern_miner import PatternMiner, _NON_ANOMALY_TYPES


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

def _make_entry(
    tool="Bash",
    model="claude:Bash",
    anomalies=None,
    resolved=True,
    session_id="test-session-1",
    ts="2026-03-24T10:30:00+00:00",
):
    return {
        "ts": ts,
        "model": model,
        "tool": tool,
        "msg_id": f"hook_{id(anomalies)}",
        "anomalies": anomalies or [],
        "resolved": resolved,
        "retries": 0,
        "session_id": session_id,
    }


def _write_jsonl(path, entries):
    with open(path, "w", encoding="utf-8") as fh:
        for entry in entries:
            fh.write(json.dumps(entry) + "\n")


# ---------------------------------------------------------------------------
# PatternMiner basics
# ---------------------------------------------------------------------------

class TestPatternMinerLoading:
    """File loading and entry parsing."""

    def test_load_single_file(self, tmp_path):
        entries = [_make_entry(), _make_entry(tool="Read")]
        f = str(tmp_path / "audit.jsonl")
        _write_jsonl(f, entries)
        miner = PatternMiner()
        count = miner.add_jsonl_file(f)
        assert count == 2

    def test_load_empty_file(self, tmp_path):
        f = str(tmp_path / "empty.jsonl")
        Path(f).touch()
        miner = PatternMiner()
        assert miner.add_jsonl_file(f) == 0

    def test_load_missing_file(self):
        miner = PatternMiner()
        assert miner.add_jsonl_file("/nonexistent/path.jsonl") == 0

    def test_load_dir(self, tmp_path):
        _write_jsonl(str(tmp_path / "session_001.jsonl"), [_make_entry()])
        _write_jsonl(str(tmp_path / "session_002.jsonl"), [_make_entry(), _make_entry()])
        # This file should NOT be loaded (wrong naming)
        _write_jsonl(str(tmp_path / "other.jsonl"), [_make_entry()])
        miner = PatternMiner()
        count = miner.add_jsonl_dir(str(tmp_path))
        assert count == 3

    def test_load_missing_dir(self):
        miner = PatternMiner()
        assert miner.add_jsonl_dir("/nonexistent/dir") == 0

    def test_load_project_dir(self, tmp_path, monkeypatch):
        _write_jsonl(
            str(tmp_path / ".insaits_audit_session.jsonl"),
            [_make_entry(), _make_entry()],
        )
        # Prevent loading real ~/.insaits/ sessions
        monkeypatch.setenv("HOME", str(tmp_path / "fakehome"))
        monkeypatch.setenv("USERPROFILE", str(tmp_path / "fakehome"))
        miner = PatternMiner()
        count = miner.add_project_dir(str(tmp_path))
        assert count == 2

    def test_bad_json_lines_skipped(self, tmp_path):
        f = str(tmp_path / "audit.jsonl")
        with open(f, "w") as fh:
            fh.write('{"valid": true}\n')
            fh.write('not valid json\n')
            fh.write('{"also": "valid"}\n')
        miner = PatternMiner()
        count = miner.add_jsonl_file(f)
        assert count == 2


class TestPatternMinerMining:
    """Core mining logic."""

    def test_empty_mine_returns_structure(self):
        miner = PatternMiner()
        result = miner.mine()
        assert "anomaly_frequency" in result
        assert "metadata" in result
        assert result["metadata"]["total_entries"] == 0

    def test_anomaly_frequency(self, tmp_path):
        entries = [
            _make_entry(anomalies=[
                {"type": "context_collapse", "severity": "HIGH"},
            ]),
            _make_entry(anomalies=[
                {"type": "context_collapse", "severity": "HIGH"},
                {"type": "blank_response", "severity": "MEDIUM"},
            ]),
            _make_entry(anomalies=[
                {"type": "unauthorized_write", "severity": "LOW"},
            ]),
        ]
        f = str(tmp_path / "audit.jsonl")
        _write_jsonl(f, entries)
        miner = PatternMiner()
        miner.add_jsonl_file(f)
        result = miner.mine()
        freq = result["anomaly_frequency"]
        assert freq["context_collapse"] == 2
        assert freq["blank_response"] == 1
        assert freq["unauthorized_write"] == 1

    def test_non_anomaly_types_excluded(self, tmp_path):
        entries = [
            _make_entry(anomalies=[
                {"type": "anchor_injected", "severity": "INFO"},
                {"type": "task_anchor_injected", "severity": "INFO"},
                {"type": "context_collapse", "severity": "HIGH"},
            ]),
        ]
        f = str(tmp_path / "audit.jsonl")
        _write_jsonl(f, entries)
        miner = PatternMiner()
        miner.add_jsonl_file(f)
        result = miner.mine()
        freq = result["anomaly_frequency"]
        assert "anchor_injected" not in freq
        assert "task_anchor_injected" not in freq
        assert freq["context_collapse"] == 1

    def test_severity_distribution(self, tmp_path):
        entries = [
            _make_entry(anomalies=[{"type": "a", "severity": "HIGH"}]),
            _make_entry(anomalies=[{"type": "b", "severity": "HIGH"}]),
            _make_entry(anomalies=[{"type": "c", "severity": "LOW"}]),
        ]
        f = str(tmp_path / "audit.jsonl")
        _write_jsonl(f, entries)
        miner = PatternMiner()
        miner.add_jsonl_file(f)
        result = miner.mine()
        assert result["severity_distribution"]["HIGH"] == 2
        assert result["severity_distribution"]["LOW"] == 1

    def test_agent_anomaly_combos(self, tmp_path):
        entries = [
            _make_entry(
                model="claude:Bash",
                anomalies=[{"type": "unauthorized_write", "severity": "LOW"}],
            ),
            _make_entry(
                model="claude:Bash",
                anomalies=[{"type": "unauthorized_write", "severity": "LOW"}],
            ),
            _make_entry(
                model="subagent:Explore",
                anomalies=[{"type": "context_collapse", "severity": "HIGH"}],
            ),
        ]
        f = str(tmp_path / "audit.jsonl")
        _write_jsonl(f, entries)
        miner = PatternMiner()
        miner.add_jsonl_file(f)
        result = miner.mine()
        combos = result["agent_anomaly_combos"]
        assert len(combos) >= 2
        top = combos[0]
        assert top["agent"] == "Bash"
        assert top["anomaly_type"] == "unauthorized_write"
        assert top["count"] == 2

    def test_resolution_stats(self, tmp_path):
        entries = [
            _make_entry(
                anomalies=[{"type": "a", "severity": "LOW"}],
                resolved=True,
            ),
            _make_entry(
                anomalies=[{"type": "a", "severity": "LOW"}],
                resolved=False,
            ),
            _make_entry(
                anomalies=[{"type": "a", "severity": "LOW"}],
                resolved=True,
            ),
        ]
        f = str(tmp_path / "audit.jsonl")
        _write_jsonl(f, entries)
        miner = PatternMiner()
        miner.add_jsonl_file(f)
        result = miner.mine()
        res = result["resolution_stats"]
        assert res["total"] == 3
        assert res["resolved"] == 2
        assert res["unresolved"] == 1
        assert res["resolution_rate"] == pytest.approx(66.7, abs=0.1)

    def test_intervention_stats(self, tmp_path):
        entries = [
            _make_entry(
                anomalies=[{
                    "type": "context_collapse",
                    "severity": "HIGH",
                    "intervention": "Re-read the task",
                }],
                resolved=False,
            ),
            _make_entry(
                anomalies=[{
                    "type": "context_collapse",
                    "severity": "HIGH",
                    "intervention": "Focus on original request",
                }],
                resolved=True,
            ),
        ]
        f = str(tmp_path / "audit.jsonl")
        _write_jsonl(f, entries)
        miner = PatternMiner()
        miner.add_jsonl_file(f)
        result = miner.mine()
        istats = result["intervention_stats"]["context_collapse"]
        assert istats["total_occurrences"] == 2
        assert istats["resolved"] == 1
        assert istats["has_intervention"] is True
        assert istats["resolution_rate"] == 50.0

    def test_tool_sequences(self, tmp_path):
        entries = [
            _make_entry(tool="Read"),  # prev_tool = ""
            _make_entry(  # prev_tool = "Read"
                tool="Bash",
                anomalies=[{"type": "unauthorized_write", "severity": "LOW"}],
            ),
        ]
        f = str(tmp_path / "audit.jsonl")
        _write_jsonl(f, entries)
        miner = PatternMiner()
        miner.add_jsonl_file(f)
        result = miner.mine()
        seq = result["tool_sequences"]
        assert "unauthorized_write" in seq
        assert seq["unauthorized_write"]["Read"] == 1

    def test_session_summaries(self, tmp_path):
        entries = [
            _make_entry(
                session_id="sess-A",
                anomalies=[{"type": "a", "severity": "LOW"}],
                ts="2026-03-24T10:00:00+00:00",
            ),
            _make_entry(
                session_id="sess-A",
                ts="2026-03-24T10:30:00+00:00",
            ),
            _make_entry(
                session_id="sess-B",
                anomalies=[
                    {"type": "a", "severity": "LOW"},
                    {"type": "b", "severity": "HIGH"},
                ],
            ),
        ]
        f = str(tmp_path / "audit.jsonl")
        _write_jsonl(f, entries)
        miner = PatternMiner()
        miner.add_jsonl_file(f)
        result = miner.mine()
        summaries = result["session_summaries"]
        assert len(summaries) == 2
        # Sorted by anomaly count desc
        assert summaries[0]["anomaly_count"] >= summaries[1]["anomaly_count"]

    def test_hourly_distribution(self, tmp_path):
        entries = [
            _make_entry(ts="2026-03-24T10:30:00+00:00"),
            _make_entry(ts="2026-03-24T10:45:00+00:00"),
            _make_entry(ts="2026-03-24T14:00:00+00:00"),
        ]
        f = str(tmp_path / "audit.jsonl")
        _write_jsonl(f, entries)
        miner = PatternMiner()
        miner.add_jsonl_file(f)
        result = miner.mine()
        hourly = result["hourly_distribution"]
        assert hourly["10"] == 2
        assert hourly["14"] == 1

    def test_metadata(self, tmp_path):
        entries = [_make_entry(), _make_entry()]
        f = str(tmp_path / "audit.jsonl")
        _write_jsonl(f, entries)
        miner = PatternMiner()
        miner.add_jsonl_file(f)
        result = miner.mine()
        meta = result["metadata"]
        assert meta["files_loaded"] == 1
        assert meta["total_entries"] == 2
        assert meta["unique_sessions"] == 1
        assert "mined_at" in meta


class TestPatternMinerSave:
    """Output file writing."""

    def test_save_creates_file(self, tmp_path):
        miner = PatternMiner()
        result = miner.mine()
        out = str(tmp_path / "output.json")
        miner.save(result, out)
        assert os.path.isfile(out)
        with open(out) as fh:
            data = json.load(fh)
        assert "metadata" in data

    def test_save_is_valid_json(self, tmp_path):
        entries = [
            _make_entry(
                anomalies=[{"type": "a", "severity": "HIGH"}],
            ),
        ]
        f = str(tmp_path / "audit.jsonl")
        _write_jsonl(f, entries)
        miner = PatternMiner()
        miner.add_jsonl_file(f)
        result = miner.mine()
        out = str(tmp_path / "patterns.json")
        miner.save(result, out)
        # Should parse without error
        with open(out) as fh:
            data = json.load(fh)
        assert data["anomaly_frequency"]["a"] == 1


class TestPatternMinerEdgeCases:
    """Edge cases and robustness."""

    def test_missing_fields_in_entry(self, tmp_path):
        """Entries with missing fields should not crash."""
        entries = [
            {"ts": "2026-03-24T10:00:00+00:00"},  # minimal
            {"anomalies": [{"type": "x"}]},  # no ts, no model
        ]
        f = str(tmp_path / "audit.jsonl")
        _write_jsonl(f, entries)
        miner = PatternMiner()
        miner.add_jsonl_file(f)
        result = miner.mine()
        assert result["metadata"]["total_entries"] == 2

    def test_multiple_sessions(self, tmp_path):
        entries = [
            _make_entry(session_id="s1"),
            _make_entry(session_id="s2"),
            _make_entry(session_id="s3"),
        ]
        f = str(tmp_path / "audit.jsonl")
        _write_jsonl(f, entries)
        miner = PatternMiner()
        miner.add_jsonl_file(f)
        result = miner.mine()
        assert result["metadata"]["unique_sessions"] == 3

    def test_large_anomaly_list(self, tmp_path):
        """Entry with many anomalies should be handled."""
        anomalies = [
            {"type": f"type_{i}", "severity": "LOW"}
            for i in range(50)
        ]
        entries = [_make_entry(anomalies=anomalies)]
        f = str(tmp_path / "audit.jsonl")
        _write_jsonl(f, entries)
        miner = PatternMiner()
        miner.add_jsonl_file(f)
        result = miner.mine()
        assert result["metadata"]["total_anomalies"] == 50

    def test_non_anomaly_types_constant(self):
        """Verify the non-anomaly type set is correct."""
        assert "anchor_injected" in _NON_ANOMALY_TYPES
        assert "task_anchor_injected" in _NON_ANOMALY_TYPES
        assert "response_awareness" in _NON_ANOMALY_TYPES
        assert "subagent_spawned" in _NON_ANOMALY_TYPES
        # Real anomalies should NOT be in the set
        assert "context_collapse" not in _NON_ANOMALY_TYPES
        assert "unauthorized_write" not in _NON_ANOMALY_TYPES


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
