"""
Tests for AlertEngine rule evaluation and API endpoints.
"""

import time
import uuid
from datetime import datetime, timedelta

import pytest

from autopil.alerting import AlertEngine
from autopil.models import AuditEvent, Decision


# ── helpers ────────────────────────────────────────────────────────────────────

def _event(decision=Decision.DENY, source_id="src_a", agent_role="analyst",
           session_id=None, minutes_ago=0) -> AuditEvent:
    return AuditEvent(
        event_id=str(uuid.uuid4()),
        session_id=session_id or str(uuid.uuid4()),
        agent_role=agent_role,
        user_id="user_1",
        source_id=source_id,
        query="test query",
        decision=decision,
        policy_name="test_policy",
        reason="test reason",
        timestamp=datetime.utcnow() - timedelta(minutes=minutes_ago),
    )


def _fire_events(alert_store, audit_log, tenant_id, events, rule):
    """Record events into audit log then run AlertEngine.check against the last one."""
    for e in events:
        audit_log.record(e, tenant_id=tenant_id)
    engine = AlertEngine(alert_store, audit_log)
    engine.check(events[-1], tenant_id)


# ── store CRUD tests ───────────────────────────────────────────────────────────

def test_create_and_get_rule(stores, tenant_id):
    _, _, _, _, alert_store, _, _ = stores
    rule_id = alert_store.create_rule(
        {"name": "Spike", "rule_type": "denial_spike", "threshold": 3, "window_minutes": 10},
        tenant_id,
    )
    rule = alert_store.get_rule(rule_id, tenant_id)
    assert rule["name"] == "Spike"
    assert rule["rule_type"] == "denial_spike"
    assert rule["threshold"] == 3
    assert rule["is_enabled"] is True
    assert rule["last_fired_at"] is None


def test_list_rules_empty(stores, tenant_id):
    _, _, _, _, alert_store, _, _ = stores
    assert alert_store.list_rules(tenant_id) == []


def test_update_rule(stores, tenant_id):
    _, _, _, _, alert_store, _, _ = stores
    rule_id = alert_store.create_rule(
        {"name": "R", "rule_type": "denial_spike"}, tenant_id
    )
    ok = alert_store.update_rule(rule_id, {"threshold": 20, "is_enabled": False}, tenant_id)
    assert ok is True
    rule = alert_store.get_rule(rule_id, tenant_id)
    assert rule["threshold"] == 20
    assert rule["is_enabled"] is False


def test_delete_rule(stores, tenant_id):
    _, _, _, _, alert_store, _, _ = stores
    rule_id = alert_store.create_rule(
        {"name": "R", "rule_type": "denial_spike"}, tenant_id
    )
    assert alert_store.delete_rule(rule_id, tenant_id) is True
    assert alert_store.get_rule(rule_id, tenant_id) is None
    assert alert_store.delete_rule(rule_id, tenant_id) is False


def test_rule_isolation_across_tenants(stores):
    _, _, tenant_store, _, alert_store, _, _ = stores
    t1 = tenant_store.create_tenant("t_alert_1")
    t2 = tenant_store.create_tenant("t_alert_2")
    alert_store.create_rule({"name": "R", "rule_type": "denial_spike"}, t1)
    assert alert_store.list_rules(t2) == []


# ── denial_spike ──────────────────────────────────────────────────────────────

def test_denial_spike_fires(stores, tenant_id):
    audit_log, _, _, _, alert_store, _, _ = stores
    alert_store.create_rule(
        {"name": "Spike", "rule_type": "denial_spike", "threshold": 3, "window_minutes": 60,
         "cooldown_minutes": 0},
        tenant_id,
    )
    events = [_event(Decision.DENY) for _ in range(3)]
    _fire_events(alert_store, audit_log, tenant_id, events, None)
    time.sleep(0.2)
    deliveries = alert_store.list_deliveries(tenant_id)
    assert len(deliveries) == 1
    assert deliveries[0]["rule_name"] == "Spike"
    assert deliveries[0]["status"] == "no_destination"
    assert deliveries[0]["detail"]["denials_in_window"] == 3


def test_denial_spike_no_fire_below_threshold(stores, tenant_id):
    audit_log, _, _, _, alert_store, _, _ = stores
    alert_store.create_rule(
        {"name": "Spike", "rule_type": "denial_spike", "threshold": 5, "window_minutes": 60,
         "cooldown_minutes": 0},
        tenant_id,
    )
    events = [_event(Decision.DENY) for _ in range(4)]
    _fire_events(alert_store, audit_log, tenant_id, events, None)
    assert alert_store.list_deliveries(tenant_id) == []


def test_denial_spike_respects_cooldown(stores, tenant_id):
    audit_log, _, _, _, alert_store, _, _ = stores
    rule_id = alert_store.create_rule(
        {"name": "Spike", "rule_type": "denial_spike", "threshold": 2, "window_minutes": 60,
         "cooldown_minutes": 999},
        tenant_id,
    )
    events = [_event(Decision.DENY) for _ in range(3)]
    for e in events:
        audit_log.record(e, tenant_id=tenant_id)

    engine = AlertEngine(alert_store, audit_log)
    # first check — should fire
    engine.check(events[-1], tenant_id)
    # second check immediately — should be suppressed by cooldown
    engine.check(events[-1], tenant_id)

    deliveries = alert_store.list_deliveries(tenant_id)
    assert len(deliveries) == 1


def test_denial_spike_ignores_allows(stores, tenant_id):
    audit_log, _, _, _, alert_store, _, _ = stores
    alert_store.create_rule(
        {"name": "Spike", "rule_type": "denial_spike", "threshold": 2, "window_minutes": 60,
         "cooldown_minutes": 0},
        tenant_id,
    )
    events = [_event(Decision.ALLOW) for _ in range(5)]
    _fire_events(alert_store, audit_log, tenant_id, events, None)
    assert alert_store.list_deliveries(tenant_id) == []


# ── new_source ────────────────────────────────────────────────────────────────

def test_new_source_fires_for_unknown_source(stores, tenant_id):
    audit_log, _, _, _, alert_store, _, _ = stores
    alert_store.create_rule(
        {"name": "NewSrc", "rule_type": "new_source", "cooldown_minutes": 0},
        tenant_id,
    )
    event = _event(Decision.ALLOW, source_id="brand_new_source")
    _fire_events(alert_store, audit_log, tenant_id, [event], None)
    time.sleep(0.2)
    deliveries = alert_store.list_deliveries(tenant_id)
    assert len(deliveries) == 1
    assert deliveries[0]["detail"]["new_source"] == "brand_new_source"


def test_new_source_no_fire_for_known_source(stores, tenant_id):
    audit_log, _, _, _, alert_store, _, _ = stores
    # seed a prior event with the same source
    prior = _event(Decision.ALLOW, source_id="known_source")
    audit_log.record(prior, tenant_id=tenant_id)

    alert_store.create_rule(
        {"name": "NewSrc", "rule_type": "new_source", "cooldown_minutes": 0},
        tenant_id,
    )
    event = _event(Decision.ALLOW, source_id="known_source")
    _fire_events(alert_store, audit_log, tenant_id, [event], None)
    assert alert_store.list_deliveries(tenant_id) == []


# ── isolation_violation ───────────────────────────────────────────────────────

def test_isolation_violation_fires(stores, tenant_id):
    audit_log, _, _, _, alert_store, _, _ = stores
    alert_store.create_rule(
        {"name": "Isolation", "rule_type": "isolation_violation", "cooldown_minutes": 0},
        tenant_id,
    )
    session = str(uuid.uuid4())
    e1 = _event(Decision.ALLOW, agent_role="analyst", session_id=session)
    e2 = _event(Decision.ALLOW, agent_role="viewer",  session_id=session)
    audit_log.record(e1, tenant_id=tenant_id)
    _fire_events(alert_store, audit_log, tenant_id, [e2], None)
    time.sleep(0.2)
    deliveries = alert_store.list_deliveries(tenant_id)
    assert len(deliveries) == 1
    assert set(deliveries[0]["detail"]["agent_roles"]) == {"analyst", "viewer"}


def test_isolation_violation_no_fire_single_role(stores, tenant_id):
    audit_log, _, _, _, alert_store, _, _ = stores
    alert_store.create_rule(
        {"name": "Isolation", "rule_type": "isolation_violation", "cooldown_minutes": 0},
        tenant_id,
    )
    session = str(uuid.uuid4())
    events = [_event(Decision.ALLOW, agent_role="analyst", session_id=session) for _ in range(3)]
    _fire_events(alert_store, audit_log, tenant_id, events, None)
    assert alert_store.list_deliveries(tenant_id) == []


# ── high_deny_rate ────────────────────────────────────────────────────────────

def test_high_deny_rate_fires(stores, tenant_id):
    audit_log, _, _, _, alert_store, _, _ = stores
    alert_store.create_rule(
        {"name": "DenyRate", "rule_type": "high_deny_rate", "threshold": 50,
         "window_minutes": 60, "cooldown_minutes": 0},
        tenant_id,
    )
    events = (
        [_event(Decision.DENY)  for _ in range(4)] +
        [_event(Decision.ALLOW) for _ in range(1)]
    )
    _fire_events(alert_store, audit_log, tenant_id, events, None)
    time.sleep(0.2)
    deliveries = alert_store.list_deliveries(tenant_id)
    assert len(deliveries) == 1
    assert deliveries[0]["detail"]["deny_rate_pct"] == 80.0


def test_high_deny_rate_no_fire_below_threshold(stores, tenant_id):
    audit_log, _, _, _, alert_store, _, _ = stores
    alert_store.create_rule(
        {"name": "DenyRate", "rule_type": "high_deny_rate", "threshold": 90,
         "window_minutes": 60, "cooldown_minutes": 0},
        tenant_id,
    )
    events = (
        [_event(Decision.DENY)  for _ in range(4)] +
        [_event(Decision.ALLOW) for _ in range(6)]
    )
    _fire_events(alert_store, audit_log, tenant_id, events, None)
    assert alert_store.list_deliveries(tenant_id) == []


# ── disabled rule ─────────────────────────────────────────────────────────────

def test_disabled_rule_does_not_fire(stores, tenant_id):
    audit_log, _, _, _, alert_store, _, _ = stores
    rule_id = alert_store.create_rule(
        {"name": "Spike", "rule_type": "denial_spike", "threshold": 2, "cooldown_minutes": 0},
        tenant_id,
    )
    alert_store.update_rule(rule_id, {"is_enabled": False}, tenant_id)
    events = [_event(Decision.DENY) for _ in range(5)]
    _fire_events(alert_store, audit_log, tenant_id, events, None)
    assert alert_store.list_deliveries(tenant_id) == []


# ── API endpoint tests ─────────────────────────────────────────────────────────

def test_api_create_alert_rule(client):
    r = client.post("/v1/alerts/rules", json={
        "name": "Spike", "rule_type": "denial_spike", "threshold": 10, "window_minutes": 30,
    })
    assert r.status_code == 200
    body = r.json()
    assert body["name"] == "Spike"
    assert body["rule_type"] == "denial_spike"
    assert body["threshold"] == 10
    assert body["is_enabled"] is True
    assert "rule_id" in body


def test_api_list_alert_rules(client):
    client.post("/v1/alerts/rules", json={"name": "R1", "rule_type": "new_source"})
    client.post("/v1/alerts/rules", json={"name": "R2", "rule_type": "isolation_violation"})
    r = client.get("/v1/alerts/rules")
    assert r.status_code == 200
    names = [x["name"] for x in r.json()]
    assert "R1" in names and "R2" in names


def test_api_update_alert_rule(client):
    rule_id = client.post("/v1/alerts/rules", json={
        "name": "R", "rule_type": "denial_spike"
    }).json()["rule_id"]
    r = client.put(f"/v1/alerts/rules/{rule_id}", json={"threshold": 99, "is_enabled": False})
    assert r.status_code == 200
    assert r.json()["threshold"] == 99
    assert r.json()["is_enabled"] is False


def test_api_delete_alert_rule(client):
    rule_id = client.post("/v1/alerts/rules", json={
        "name": "R", "rule_type": "denial_spike"
    }).json()["rule_id"]
    assert client.delete(f"/v1/alerts/rules/{rule_id}").status_code == 200
    assert client.delete(f"/v1/alerts/rules/{rule_id}").status_code == 404


def test_api_fired_alerts_after_evaluate(client):
    # create a denial_spike rule with threshold=1
    r = client.post("/v1/alerts/rules", json={
        "name": "HairTrigger", "rule_type": "denial_spike",
        "threshold": 1, "window_minutes": 60, "cooldown_minutes": 0,
    })
    assert r.status_code == 200, r.json()
    # trigger a denial — analyst querying executive_comms is always DENY
    client.post("/v1/context/evaluate", json={
        "query": "get secret doc", "agent_role": "analyst",
        "user_id": "u1", "source_id": "executive_comms",
        "sensitivity_level": "high",
    })
    r = client.get("/v1/alerts/fired")
    assert r.status_code == 200
    fired = r.json()
    assert len(fired) >= 1
    assert fired[0]["rule_name"] == "HairTrigger"
    assert fired[0]["status"] == "no_destination"  # no webhook/email configured
