"""
Tests for Phase 34: Immutable Audit Trail.
"""

import json
import time
from datetime import datetime, timedelta, timezone

import pytest


@pytest.fixture(autouse=True)
def _isolate(tmp_path, monkeypatch):
    """Redirect all audit trail storage to a temp directory."""
    audit_dir = tmp_path / "audit"
    archive_dir = audit_dir / "archive"
    trail_path = audit_dir / "trail.jsonl"
    for d in (audit_dir, archive_dir):
        d.mkdir(parents=True, exist_ok=True)

    import brynq.server as srv
    monkeypatch.setattr(srv, "BROKER_DIR", tmp_path)

    import brynq.audit_trail as at
    monkeypatch.setattr(at, "AUDIT_DIR", audit_dir)
    monkeypatch.setattr(at, "TRAIL_PATH", trail_path)
    monkeypatch.setattr(at, "ARCHIVE_DIR", archive_dir)


# ── Helpers ────────────────────────────────────────────────────────────────


def _log(action="task_created", actor_id="agent-1", actor_name="Agent One",
         details=None, target=None, target_type=None):
    from brynq.audit_trail import log_event
    return log_event(action, actor_id, actor_name, details, target, target_type)


# ── 1. Hash-Chained Event Logging ─────────────────────────────────────────


class TestLogEvent:
    def test_log_single_event(self):
        entry = _log()
        assert entry["action"] == "task_created"
        assert entry["actor_id"] == "agent-1"
        assert entry["actor_name"] == "Agent One"
        assert entry["prev_hash"] == "0" * 64
        assert len(entry["hash"]) == 64
        assert entry["id"]

    def test_log_chain_links(self):
        e1 = _log(action="task_created")
        e2 = _log(action="task_completed")
        assert e2["prev_hash"] == e1["hash"]
        assert e2["hash"] != e1["hash"]

    def test_log_with_details_and_target(self):
        entry = _log(
            action="file_claimed",
            details={"path": "/src/main.py"},
            target="file-123",
            target_type="file",
        )
        assert entry["details"]["path"] == "/src/main.py"
        assert entry["target"] == "file-123"
        assert entry["target_type"] == "file"

    def test_invalid_action_raises(self):
        from brynq.audit_trail import log_event
        with pytest.raises(ValueError, match="Invalid action"):
            log_event("not_a_real_action", "a1", "Agent")

    def test_genesis_hash(self):
        """First entry in the chain should have prev_hash of all zeros."""
        entry = _log()
        assert entry["prev_hash"] == "0" * 64


# ── 2. Chain Verification ─────────────────────────────────────────────────


class TestVerifyChain:
    def test_empty_chain_valid(self):
        from brynq.audit_trail import verify_chain
        valid, broken_at, total = verify_chain()
        assert valid is True
        assert broken_at is None
        assert total == 0

    def test_single_entry_valid(self):
        from brynq.audit_trail import verify_chain
        _log()
        valid, broken_at, total = verify_chain()
        assert valid is True
        assert broken_at is None
        assert total == 1

    def test_multi_entry_valid(self):
        from brynq.audit_trail import verify_chain
        for i in range(5):
            _log(action="task_created", actor_id=f"agent-{i}")
        valid, broken_at, total = verify_chain()
        assert valid is True
        assert total == 5

    def test_tamper_detection_modify_action(self):
        """Modifying a past entry's action should break the chain."""
        from brynq.audit_trail import verify_chain, TRAIL_PATH

        _log(action="task_created")
        _log(action="task_completed")
        _log(action="message_sent")

        # Tamper with the second entry
        lines = TRAIL_PATH.read_text("utf-8").strip().splitlines()
        entries = [json.loads(line) for line in lines]
        entries[1]["action"] = "agent_exiled"  # Tamper!
        TRAIL_PATH.write_text(
            "\n".join(json.dumps(e) for e in entries) + "\n",
            encoding="utf-8",
        )

        valid, broken_at, total = verify_chain()
        assert valid is False
        assert broken_at == 1
        assert total == 3

    def test_tamper_detection_modify_hash(self):
        """Modifying a hash directly should break the chain."""
        from brynq.audit_trail import verify_chain, TRAIL_PATH

        _log()
        _log()

        lines = TRAIL_PATH.read_text("utf-8").strip().splitlines()
        entries = [json.loads(line) for line in lines]
        entries[0]["hash"] = "a" * 64  # Tamper with hash
        TRAIL_PATH.write_text(
            "\n".join(json.dumps(e) for e in entries) + "\n",
            encoding="utf-8",
        )

        valid, broken_at, total = verify_chain()
        assert valid is False
        # Chain breaks at entry 0 because the recomputed hash no longer matches
        assert broken_at == 0
        assert total == 2

    def test_tamper_detection_modify_prev_hash(self):
        """Modifying prev_hash on the first entry breaks the chain."""
        from brynq.audit_trail import verify_chain, TRAIL_PATH

        _log()

        lines = TRAIL_PATH.read_text("utf-8").strip().splitlines()
        entries = [json.loads(line) for line in lines]
        entries[0]["prev_hash"] = "b" * 64  # Not the genesis hash
        TRAIL_PATH.write_text(
            "\n".join(json.dumps(e) for e in entries) + "\n",
            encoding="utf-8",
        )

        valid, broken_at, total = verify_chain()
        assert valid is False
        assert broken_at == 0


# ── 3. Query API ──────────────────────────────────────────────────────────


class TestQueryAPI:
    def test_get_events_all(self):
        from brynq.audit_trail import get_events
        _log(action="task_created")
        _log(action="task_completed")
        _log(action="message_sent")
        events = get_events()
        assert len(events) == 3

    def test_filter_by_action(self):
        from brynq.audit_trail import get_events
        _log(action="task_created")
        _log(action="task_completed")
        _log(action="task_created")
        events = get_events(action="task_created")
        assert len(events) == 2
        assert all(e["action"] == "task_created" for e in events)

    def test_filter_by_actor(self):
        from brynq.audit_trail import get_events
        _log(actor_id="alice")
        _log(actor_id="bob")
        _log(actor_id="alice")
        events = get_events(actor_id="alice")
        assert len(events) == 2

    def test_filter_by_target(self):
        from brynq.audit_trail import get_events
        _log(target="task-1")
        _log(target="task-2")
        _log(target="task-1")
        events = get_events(target="task-1")
        assert len(events) == 2

    def test_filter_by_date_range(self):
        from brynq.audit_trail import get_events
        _log()
        _log()
        now = datetime.now(timezone.utc)
        future = (now + timedelta(hours=1)).isoformat()
        past = (now - timedelta(hours=1)).isoformat()
        events = get_events(since=past, until=future)
        assert len(events) == 2

        # Far future — no events
        far_future = (now + timedelta(days=10)).isoformat()
        events = get_events(since=far_future)
        assert len(events) == 0

    def test_limit(self):
        from brynq.audit_trail import get_events
        for _ in range(10):
            _log()
        events = get_events(limit=3)
        assert len(events) == 3

    def test_newest_first(self):
        from brynq.audit_trail import get_events
        _log(action="task_created")
        _log(action="task_completed")
        events = get_events()
        # Newest first — last logged should be first in results
        assert events[0]["action"] == "task_completed"

    def test_get_actor_history(self):
        from brynq.audit_trail import get_actor_history
        _log(actor_id="alice", action="task_created")
        _log(actor_id="bob", action="task_completed")
        _log(actor_id="alice", action="message_sent")
        history = get_actor_history("alice")
        assert len(history) == 2
        assert all(e["actor_id"] == "alice" for e in history)

    def test_get_target_history(self):
        from brynq.audit_trail import get_target_history
        _log(target="task-42", action="task_created")
        _log(target="task-99", action="task_completed")
        _log(target="task-42", action="task_completed")
        history = get_target_history("task-42")
        assert len(history) == 2
        assert all(e["target"] == "task-42" for e in history)

    def test_count_events(self):
        from brynq.audit_trail import count_events
        _log(action="task_created", actor_id="alice")
        _log(action="task_completed", actor_id="bob")
        _log(action="task_created", actor_id="alice")

        assert count_events() == 3
        assert count_events(action="task_created") == 2
        assert count_events(actor_id="bob") == 1
        assert count_events(action="task_created", actor_id="alice") == 2


# ── 4. Export ──────────────────────────────────────────────────────────────


class TestExport:
    def test_export_jsonl(self):
        from brynq.audit_trail import export_events
        _log(action="task_created")
        _log(action="task_completed")
        output = export_events(format="jsonl")
        lines = output.strip().split("\n")
        assert len(lines) == 2
        # Oldest first in export
        parsed = json.loads(lines[0])
        assert parsed["action"] == "task_created"

    def test_export_csv(self):
        from brynq.audit_trail import export_events
        _log(action="login", actor_id="user-1", actor_name="User One")
        _log(action="logout", actor_id="user-1", actor_name="User One")
        output = export_events(format="csv")
        lines = output.strip().split("\n")
        assert len(lines) == 3  # header + 2 rows
        assert "id,timestamp,action" in lines[0]

    def test_export_empty(self):
        from brynq.audit_trail import export_events
        output = export_events(format="jsonl")
        assert output == ""

    def test_export_csv_empty(self):
        from brynq.audit_trail import export_events
        output = export_events(format="csv")
        assert output == ""

    def test_export_invalid_format(self):
        from brynq.audit_trail import export_events
        with pytest.raises(ValueError, match="Unsupported format"):
            export_events(format="xml")


# ── 5. Rotation ───────────────────────────────────────────────────────────


class TestRotation:
    def test_rotate_empty(self):
        from brynq.audit_trail import rotate_log
        result = rotate_log()
        assert result["archived_count"] == 0
        assert result["kept_count"] == 0
        assert result["archive_file"] is None

    def test_rotate_archives_old(self):
        from brynq.audit_trail import rotate_log, TRAIL_PATH, ARCHIVE_DIR

        # Write entries with old timestamps directly
        old_ts = (datetime.now(timezone.utc) - timedelta(days=100)).isoformat()
        new_ts = datetime.now(timezone.utc).isoformat()

        old_entry = {
            "id": "old1", "timestamp": old_ts, "action": "login",
            "actor_id": "a1", "actor_name": "A1", "details": {},
            "target": None, "target_type": None,
            "prev_hash": "0" * 64, "hash": "a" * 64,
        }
        new_entry = {
            "id": "new1", "timestamp": new_ts, "action": "logout",
            "actor_id": "a1", "actor_name": "A1", "details": {},
            "target": None, "target_type": None,
            "prev_hash": "a" * 64, "hash": "b" * 64,
        }

        from brynq.server import _append_jsonl_sync
        _append_jsonl_sync(TRAIL_PATH, old_entry)
        _append_jsonl_sync(TRAIL_PATH, new_entry)

        result = rotate_log(keep_days=90)
        assert result["archived_count"] == 1
        assert result["kept_count"] == 1
        assert result["archive_file"] is not None

        # Active trail should only have the new entry
        remaining = TRAIL_PATH.read_text("utf-8").strip().splitlines()
        assert len(remaining) == 1
        assert json.loads(remaining[0])["id"] == "new1"

        # Archive should have the old entry
        archive_files = list(ARCHIVE_DIR.glob("*.jsonl"))
        assert len(archive_files) == 1

    def test_rotate_nothing_to_archive(self):
        from brynq.audit_trail import rotate_log
        # Log a fresh event — should not get archived
        _log()
        result = rotate_log(keep_days=90)
        assert result["archived_count"] == 0
        assert result["kept_count"] == 1
        assert result["archive_file"] is None


# ── 6. Stats ──────────────────────────────────────────────────────────────


class TestStats:
    def test_stats_empty(self):
        from brynq.audit_trail import get_stats
        stats = get_stats()
        assert stats["total_events"] == 0
        assert stats["date_range"]["earliest"] is None
        assert stats["events_per_action"] == {}
        assert stats["top_actors"] == []

    def test_stats_populated(self):
        from brynq.audit_trail import get_stats
        _log(action="task_created", actor_id="alice", actor_name="Alice")
        _log(action="task_created", actor_id="alice", actor_name="Alice")
        _log(action="task_completed", actor_id="bob", actor_name="Bob")

        stats = get_stats()
        assert stats["total_events"] == 3
        assert stats["events_per_action"]["task_created"] == 2
        assert stats["events_per_action"]["task_completed"] == 1
        assert stats["date_range"]["earliest"] is not None
        assert stats["date_range"]["latest"] is not None
        # Alice has 2, Bob has 1 — Alice should be first
        assert stats["top_actors"][0]["actor_id"] == "alice"
        assert stats["top_actors"][0]["count"] == 2


# ── 7. Suspicious Activity Detection ──────────────────────────────────────


class TestSuspiciousActivity:
    def test_no_suspicious_activity(self):
        from brynq.audit_trail import get_suspicious_activity
        _log(action="task_created")
        alerts = get_suspicious_activity()
        assert alerts == []

    def test_excessive_failures(self):
        from brynq.audit_trail import get_suspicious_activity
        for _ in range(6):
            _log(action="task_failed", actor_id="bad-agent", actor_name="Bad Agent")
        alerts = get_suspicious_activity()
        failure_alerts = [a for a in alerts if a["alert_type"] == "excessive_failures"]
        assert len(failure_alerts) == 1
        assert failure_alerts[0]["actor_id"] == "bad-agent"
        assert failure_alerts[0]["count"] == 6

    def test_rapid_logins(self):
        from brynq.audit_trail import get_suspicious_activity
        for _ in range(11):
            _log(action="login", actor_id="bruter", actor_name="Brute Force Bot")
        alerts = get_suspicious_activity()
        login_alerts = [a for a in alerts if a["alert_type"] == "rapid_logins"]
        assert len(login_alerts) == 1
        assert login_alerts[0]["actor_id"] == "bruter"

    def test_mass_file_claims(self):
        from brynq.audit_trail import get_suspicious_activity
        for _ in range(21):
            _log(action="file_claimed", actor_id="hoarder", actor_name="Hoarder")
        alerts = get_suspicious_activity()
        claim_alerts = [a for a in alerts if a["alert_type"] == "mass_file_claims"]
        assert len(claim_alerts) == 1

    def test_violation_spike(self):
        from brynq.audit_trail import get_suspicious_activity
        for _ in range(4):
            _log(action="violation_recorded", actor_id="violator", actor_name="Violator")
        alerts = get_suspicious_activity()
        spike_alerts = [a for a in alerts if a["alert_type"] == "violation_spike"]
        assert len(spike_alerts) == 1

    def test_below_threshold_no_alert(self):
        from brynq.audit_trail import get_suspicious_activity
        # Below all thresholds
        for _ in range(4):
            _log(action="task_failed", actor_id="ok-agent", actor_name="OK Agent")
        alerts = get_suspicious_activity()
        assert alerts == []


# ── 8. Valid Actions ──────────────────────────────────────────────────────


class TestValidActions:
    def test_all_valid_actions(self):
        from brynq.audit_trail import VALID_ACTIONS
        for action in VALID_ACTIONS:
            entry = _log(action=action)
            assert entry["action"] == action
