import hashlib
from datetime import datetime, timedelta

import pytest
from fastapi.testclient import TestClient

from app.engine import Engine
from app.main import create_app
from app.repository import InMemoryRepository

KEY = "kunci-sensor-uji-123"
NOW = datetime(2026, 10, 24, 12, 0, 0)


def make_repo():
    return InMemoryRepository(sensors=[{
        "id": 1, "sensor_code": "test-sensor", "hostname": "h", "os": "linux", "status": "online",
        "api_key_hash": hashlib.sha256(KEY.encode()).hexdigest(), "last_seen_at": NOW}])


@pytest.fixture
def ctx():
    repo = make_repo()
    app = create_app(repo, start_scheduler=False)
    app.state.engine.clock = lambda: NOW
    return repo, TestClient(app), app.state.engine


def post(client, events, key=KEY, code="test-sensor"):
    return client.post("/api/v1/ingest", headers={"X-Sensor-Key": key},
                       json={"sensor_code": code, "events": events})


def test_rejects_bad_key_and_unknown_sensor(ctx):
    _, c, _ = ctx
    assert post(c, [], key="salah").status_code == 401
    assert post(c, [], code="tidak-ada").status_code == 401


def test_rejects_invalid_event_type_and_oversize(ctx):
    _, c, _ = ctx
    assert post(c, [{"type": "rm -rf"}]).status_code == 422
    assert post(c, [{"type": "heartbeat"}] * 501).status_code == 422


def test_sqli_creates_event_attack_alert_with_incident_code(ctx):
    repo, c, _ = ctx
    r = post(c, [{"type": "http_request", "ip": "203.0.113.9", "method": "GET", "path": "/p.php",
                  "query": "id=1' OR '1'='1' UNION SELECT password FROM users&token=SECRET99"}])
    assert r.status_code == 200 and r.json()["findings"] >= 1
    ev = next(e for e in repo.events if e["event_type"] == "sql_injection")
    assert ev["incident_code"].startswith("INC-20261024-")
    assert "SECRET99" not in (ev["evidence"] or "")
    assert repo.attacks[0]["verification_status"] == "suspected"
    assert repo.alerts and repo.alerts[0]["priority"] in ("high", "critical")


def test_brute_force_via_login_events(ctx):
    repo, c, _ = ctx
    evs = [{"type": "login", "ip": "198.51.100.7", "username": "admin", "success": False}] * 6
    post(c, evs)
    assert any(e["event_type"] == "brute_force" for e in repo.events)
    assert len(repo.logins) == 6
    # dedup: tidak membuat event ganda dalam jendela dedup
    assert sum(e["event_type"] == "brute_force" for e in repo.events) == 1


def test_port_scan_from_firewall_events(ctx):
    repo, c, _ = ctx
    post(c, [{"type": "firewall", "ip": "192.0.2.50", "dst_port": p} for p in range(1000, 1020)])
    assert any(e["event_type"] == "port_scan" for e in repo.events)


def test_file_integrity_baseline_then_tamper(ctx):
    repo, c, _ = ctx
    post(c, [{"type": "file_integrity", "files": [{"path": "/etc/passwd", "hash": "a" * 64}]}])
    assert repo.fim[-1]["status"] == "new" and not repo.events
    post(c, [{"type": "file_integrity", "files": [{"path": "/etc/passwd", "hash": "a" * 64}]}])
    assert repo.fim[-1]["status"] == "ok"
    post(c, [{"type": "file_integrity", "files": [{"path": "/etc/passwd", "hash": "b" * 64}]}])
    assert repo.fim[-1]["status"] == "modified"
    assert any(e["event_type"] == "file_tampering" and e["risk_level"] == "critical" for e in repo.events)


def test_repeated_tamper_reports_once_until_hash_changes(ctx):
    repo, c, _ = ctx
    f = lambda h: post(c, [{"type": "file_integrity", "files": [{"path": "/etc/x", "hash": h * 64}]}])
    f("a"); f("b"); f("b"); f("b")
    assert sum(e["event_type"] == "file_tampering" for e in repo.events) == 1
    f("c")                                    # hash berubah lagi -> temuan baru
    assert sum(e["event_type"] == "file_tampering" for e in repo.events) == 2


def test_metrics_stored_and_high_usage_warned(ctx):
    repo, c, _ = ctx
    post(c, [{"type": "metrics", "metrics": {"cpu": 95, "mem": 40, "disk": 50}}])
    assert repo.metrics[0]["cpu_percent"] == 95
    assert any(e["event_type"] == "system_health" for e in repo.events)


def test_sensor_failure_scheduled_check(ctx):
    repo, _, engine = ctx
    repo.sensors[1]["last_seen_at"] = NOW - timedelta(seconds=900)
    out = engine.run_scheduled()
    assert out["sensor_offline"] == 1
    assert repo.sensors[1]["status"] == "offline"
    assert any(e["event_type"] == "sensor_failure" for e in repo.events)
    assert engine.run_scheduled()["sensor_offline"] == 0   # tidak mengulang alert


def test_traffic_spike_scheduled_check(ctx):
    repo, c, engine = ctx
    # isi riwayat 3 menit sebelumnya (~100/menit) lalu lonjakan 600 pada menit terakhir
    base = NOW.replace(second=0)
    for i, n in ((5, 100), (4, 110), (3, 90), (1, 600)):
        engine._req_per_min[base - timedelta(minutes=i)] = n
    out = engine.run_scheduled()
    assert out["traffic_spike"] == 1


def test_benign_traffic_creates_no_findings(ctx):
    repo, c, _ = ctx
    r = post(c, [{"type": "http_request", "ip": "10.0.0.5", "method": "GET", "path": "/index.php", "query": "page=2"}])
    assert r.json()["findings"] == 0 and not repo.events
