import json
import tempfile
import unittest
from pathlib import Path

from core.database import EventDatabase
from core.rules import RuleEngine
from gui.adapters import DataAdapter, validate_config


def write_policy(path: Path, *, allow=None, watch=None, commands=None, parents=None):
    payload = {
        "Enabled": True, "AllowList": allow or [], "WatchList": watch or [],
        "AlertRules": {"HighRiskKeywords": [], "CommandLineIndicators": commands or [],
                       "ParentChildRules": parents or []},
        "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 TestMultiRuleDetection(unittest.TestCase):
    def setUp(self):
        self.tmp = tempfile.TemporaryDirectory(); self.root = Path(self.tmp.name); self.path = self.root / "config.json"
        self.command_rules = [
            {"RuleId": "CMD-ENC", "Indicator": "-enc", "Severity": "high", "RiskContribution": 70},
            {"RuleId": "CMD-B64", "Indicator": "BASE64", "Severity": "medium", "RiskContribution": 25},
        ]
        self.parent_rules = [{"RuleId": "CFG-PARENT-CHILD", "Parent": "winword.exe", "Child": "powershell.exe",
                              "Severity": "medium", "RiskContribution": 25}]
        write_policy(self.path, allow=["trusted.exe"], watch=["powershell.exe"],
                     commands=self.command_rules, parents=self.parent_rules)
        self.engine = RuleEngine(self.path)

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

    def test_zero_and_one_match(self):
        normal = self.engine.classify({"process_name": "ordinary.exe"})
        self.assertEqual(("NORMAL", "info", 0, []), (normal["classification"], normal["severity"], normal["risk_score"], normal["matched_rules"]))
        one = self.engine.classify({"process_name": "ordinary.exe", "command_line": "tool -ENC payload"})
        self.assertEqual(["CMD-ENC"], [item["rule_id"] for item in one["matched_rules"]])
        self.assertEqual(70, one["risk_score"])

    def test_multiple_independent_matches_and_evidence(self):
        decision = self.engine.classify({"timestamp": "2026-08-14T12:00:00Z", "process_name": "powershell.exe",
            "parent_process_name": "WINWORD.EXE", "command_line": "powershell -enc BASE64"})
        self.assertEqual({"CMD-ENC", "CMD-B64", "CFG-PARENT-CHILD", "CFG-WATCHLIST"},
                         {item["rule_id"] for item in decision["matched_rules"]})
        self.assertEqual(90, decision["risk_score"])
        parent = next(item for item in decision["matched_rules"] if item["category"] == "parent_child")
        self.assertEqual({"parent_process": "WINWORD.EXE", "child_process": "powershell.exe"}, parent["evidence"])
        self.assertTrue({"rule_id", "category", "severity", "risk_contribution", "reason", "evidence", "timestamp"}.issubset(parent))

    def test_command_matches_are_case_insensitive_and_record_each_indicator(self):
        findings = self.engine.classify({"process_name": "tool.exe", "command_line": "-ENC base64"})["matched_rules"]
        self.assertEqual({"-enc", "BASE64"}, {item["evidence"]["indicator"] for item in findings})
        self.assertEqual(70, self.engine.correlated_risk(findings))  # same category adds no bonus

    def test_missing_data_and_normal_relationship_are_neutral(self):
        for event in ({"process_name": "powershell.exe", "parent_process_name": None},
                      {"process_name": "child.exe", "parent_process_name": "explorer.exe"},
                      {"process_name": "ordinary.exe", "command_line": None}):
            decision = self.engine.classify(event)
            expected = 25 if event["process_name"] == "powershell.exe" else 0
            self.assertEqual(expected, decision["risk_score"])
        self.assertEqual("TRUSTED", self.engine.classify({"process_name": "trusted.exe"})["classification"])
        detected = self.engine.classify({"process_name": "trusted.exe", "command_line": "-enc"})
        self.assertEqual("HIGH_RISK", detected["classification"])

    def test_duplicates_do_not_inflate_and_score_caps(self):
        finding = {"rule_id": "A", "category": "one", "risk_contribution": 95}
        self.assertEqual(95, self.engine.correlated_risk([finding, dict(finding)]))
        findings = [finding, {"rule_id": "B", "category": "two", "risk_contribution": 25},
                    {"rule_id": "C", "category": "three", "risk_contribution": 25}]
        self.assertEqual(100, self.engine.correlated_risk(findings))

    def test_gui_process_and_alert_detail_transformations(self):
        db = EventDatabase(self.root / "events.db")
        finding = self.engine.classify({"timestamp": "2026-08-14T12:00:00Z", "process_name": "tool.exe", "command_line": "-enc"})
        metadata = {"classification": finding["classification"], "classification_reason": finding["classification_reason"],
                    "matched_rules": finding["matched_rules"], "parent_process_name": "parent.exe"}
        db.insert_event({"event_id": "evt", "timestamp": "2026-08-14T12:00:00Z", "event_type": "process_started",
            "source": "test", "severity": "high", "pid": 7, "process_name": "tool.exe", "command_line": "-enc",
            "risk_score": 70, "tags": [], "metadata": metadata})
        db.insert_alert({"alert_id": "alert", "timestamp": "2026-08-14T12:00:00Z", "severity": "high", "title": "finding",
                         "description": "matched", "rule_id": "CMD-ENC", "process_id": 7, "event_id": "evt"})
        adapter = DataAdapter(self.root / "events.db", self.path)
        process = adapter.process_details(adapter.active_processes()[0])
        alert = adapter.alert_details(adapter.alerts()[0])
        self.assertEqual("CMD-ENC", process["matched_rules"][0]["rule_id"])
        self.assertEqual("CMD-ENC", alert["matched_rules"][0]["rule_id"])
        self.assertEqual("parent.exe", alert["parent_process_name"])


class TestStructuredConfiguration(unittest.TestCase):
    def test_empty_structured_rules_are_valid(self):
        payload = {"Enabled": True, "AllowList": [], "WatchList": [],
                   "AlertRules": {"HighRiskKeywords": [], "CommandLineIndicators": [], "ParentChildRules": []},
                   "Telemetry": {"Path": "events.jsonl", "PollIntervalSeconds": 1,
                                 "HeartbeatIntervalSeconds": 5, "RetentionDays": 1}}
        self.assertEqual([], validate_config(payload))

    def test_malformed_rules_fail_validation_and_engine_fails_safe(self):
        payload = {"Enabled": True, "AllowList": [], "WatchList": [],
                   "AlertRules": {"HighRiskKeywords": [], "CommandLineIndicators": [{"RuleId": "BAD"}],
                                  "ParentChildRules": [{"RuleId": "BAD", "Parent": "x"}]},
                   "Telemetry": {"Path": "events.jsonl", "PollIntervalSeconds": 1,
                                 "HeartbeatIntervalSeconds": 5, "RetentionDays": 1}}
        self.assertGreaterEqual(len(validate_config(payload)), 2)
        with tempfile.TemporaryDirectory() as tmp:
            path = Path(tmp) / "bad.json"; path.write_text(json.dumps(payload), encoding="utf-8")
            self.assertEqual("NORMAL", RuleEngine(path).classify({"process_name": "ordinary.exe"})["classification"])


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