import json
import tempfile
import unittest
from datetime import datetime, timedelta, timezone
from pathlib import Path

from core.database import EventDatabase
from core.rules import RuleEngine
from core.service_runtime import TelemetryRelayService
from core.threat import calculate_threat_score
from gui.adapters import DataAdapter


def policy(path: Path):
    payload = {"Enabled": True, "AllowList": ["trusted.exe"], "WatchList": ["watched.exe"],
               "AlertRules": {"HighRiskKeywords": ["-enc", "python -c"]},
               "Telemetry": {"Path": str(path.parent / "events.jsonl"), "DatabasePath": str(path.parent / "events.db"),
                             "PollIntervalSeconds": 1, "HeartbeatIntervalSeconds": 5, "RetentionDays": 14}}
    path.write_text(json.dumps(payload), encoding="utf-8")
    return payload


class TestDetectionModel(unittest.TestCase):
    def setUp(self):
        self.tempdir = tempfile.TemporaryDirectory(); self.root = Path(self.tempdir.name)
        self.config = self.root / "config.json"; policy(self.config); self.engine = RuleEngine(self.config)

    def tearDown(self): self.tempdir.cleanup()

    def test_deterministic_classifications_and_reasons(self):
        cases = [
            ({"process_name": "trusted.exe"}, "TRUSTED", "Explicit allowlist match"),
            ({"process_name": "ordinary.exe"}, "NORMAL", "No detection rules matched"),
            ({"process_name": "watched.exe"}, "WATCHLISTED", "Configured watchlist match"),
            ({"process_name": "ordinary.exe", "command_line": "powershell -enc Zg=="}, "HIGH_RISK", "High-risk command-line keyword matched"),
            ({"process_name": ""}, "UNKNOWN", "Insufficient process context"),
        ]
        for event, category, reason in cases:
            with self.subTest(category=category):
                decision = self.engine.classify(event)
                self.assertEqual(category, decision["classification"])
                self.assertEqual(reason, decision["classification_reason"])

    def test_normal_lifecycle_events_are_info_and_zero_risk(self):
        service = TelemetryRelayService(config_path=self.config)
        started = service.run_cycle({}, {100: "ordinary.exe"})[0]
        exited = service.run_cycle({100: "ordinary.exe"}, {})[0]
        self.assertEqual(("info", 0, False), (started["severity"], started["risk_score"], started["triggered_rule"]))
        self.assertEqual(("info", 0, False), (exited["severity"], exited["risk_score"], exited["triggered_rule"]))

    def test_watchlist_and_high_risk_findings_create_alerts(self):
        service = TelemetryRelayService(config_path=self.config)
        watched = service.run_cycle({}, {200: "watched.exe"})[0]
        self.assertEqual("WATCHLISTED", watched["classification"])
        alerts = service.database.list_alerts()
        self.assertEqual(1, len(alerts)); self.assertEqual("CFG-WATCHLIST", alerts[0]["rule_id"])
        high = self.engine.classify({"process_name": "ordinary.exe", "command_line": "python -c print(1)"})
        event = {"event_id": "high-1", "timestamp": datetime.now(timezone.utc).isoformat(), "source": "test",
                 "process_name": "ordinary.exe", "pid": 201}
        alert = self.engine.build_alert(event, high)
        self.assertIsNotNone(alert); self.assertEqual("high", alert["severity"])

    def test_gui_adapter_uses_authoritative_classification(self):
        db = EventDatabase(self.root / "events.db")
        base = {"timestamp": datetime.now(timezone.utc).isoformat(), "event_type": "process_started", "source": "test",
                "severity": "medium", "confidence": 0, "pid": 7, "process_name": "ordinary.exe", "risk_score": 80,
                "tags": [], "metadata": {}}
        db.insert_event({**base, "event_id": "legacy-inflated"})
        item = DataAdapter(self.root / "events.db", self.config).active_processes()[0]
        self.assertEqual("NORMAL", item["classification"])
        self.assertEqual("info", item["severity"])
        self.assertEqual(0, item["risk_score"])


class TestThreatScore(unittest.TestCase):
    def setUp(self): self.now = datetime(2026, 8, 14, 12, 0, tzinfo=timezone.utc)

    def finding(self, **updates):
        item = {"timestamp": self.now.isoformat(), "rule_id": "RULE-1", "process": "sample.exe",
                "severity": "medium", "risk_score": 25, "reason": "Configured watchlist match"}
        item.update(updates); return item

    def test_normal_event_volume_and_zero_alerts_stay_low(self):
        normal = [{"timestamp": self.now.isoformat(), "event_type": "process_started", "severity": "info", "risk_score": 0} for _ in range(1000)]
        result = calculate_threat_score(normal, now=self.now)
        self.assertEqual({"score": 0, "level": "LOW", "factors": []}, result)
        self.assertEqual("LOW", calculate_threat_score([], now=self.now)["level"])

    def test_duplicate_findings_do_not_inflate(self):
        result = calculate_threat_score([self.finding(), self.finding(timestamp=(self.now-timedelta(minutes=1)).isoformat())], now=self.now)
        self.assertEqual(25, result["score"]); self.assertEqual(1, len(result["factors"]))

    def test_old_findings_age_out(self):
        old = self.finding(timestamp=(self.now-timedelta(hours=25)).isoformat())
        self.assertEqual(0, calculate_threat_score([old], now=self.now)["score"])

    def test_score_is_capped_and_factors_explain_it(self):
        findings = [self.finding(rule_id=f"RULE-{index}", process=f"p{index}.exe", risk_score=70, reason=f"reason {index}") for index in range(5)]
        result = calculate_threat_score(findings, now=self.now)
        self.assertEqual(100, result["score"]); self.assertEqual("CRITICAL", result["level"])
        self.assertEqual(5, len(result["factors"]))
        self.assertTrue(all({"rule_id", "process", "severity", "contribution", "timestamp", "reason"}.issubset(item) for item in result["factors"]))


if __name__ == "__main__": unittest.main()
