"""Tests for modules/stealth/ids_tester — risk assessment, rule loading, preflight checks.

Validates action risk assessment, IDS rule parsing and matching,
caplet assessment, preflight go/nogo decisions, and risk escalation
without requiring Snort/Suricata or network access.
"""

import os
import time
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from modules.base import BaseModule
from modules.stealth.ids_tester import (
    IDSTester, RiskAssessment,
    ACTION_RISK_BASELINE, CAPLET_ACTION_RISK,
    RISK_LOW, RISK_MEDIUM, RISK_HIGH, RISK_CRITICAL,
)


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture
def ids(mock_bus, mock_state, tmp_path):
    """Return a started IDSTester with no rules loaded."""
    config = {"rules_dir": str(tmp_path / "rules")}
    mod = IDSTester(mock_bus, mock_state, config)
    mod.start()
    return mod


@pytest.fixture
def ids_with_rules(mock_bus, mock_state, tmp_path):
    """Return a started IDSTester with sample Snort rules."""
    rules_dir = tmp_path / "rules"
    rules_dir.mkdir()

    (rules_dir / "local.rules").write_text(
        'alert tcp any any -> any any (msg:"LLMNR Spoof Detected"; sid:1000001; classtype:bad-unknown;)\n'
        'alert udp any any -> any any (msg:"NBT-NS Poisoning"; sid:1000002; classtype:bad-unknown;)\n'
        'alert tcp any any -> any 445 (msg:"SMB Relay Detected"; sid:1000003; classtype:attempted-admin;)\n'
        'alert tcp any any -> any 80 (msg:"HTTP MITM Detected"; sid:1000004; classtype:web-application-attack;)\n'
        '# This is a comment line\n'
        '\n'
    )
    config = {"rules_dir": str(rules_dir)}
    mod = IDSTester(mock_bus, mock_state, config)
    mod.start()
    return mod


# ---------------------------------------------------------------------------
# Structure
# ---------------------------------------------------------------------------

class TestIDSTesterStructure:

    def test_is_base_module(self):
        assert issubclass(IDSTester, BaseModule)

    def test_module_attributes(self):
        assert IDSTester.name == "ids_tester"
        assert IDSTester.module_type == "stealth"
        assert IDSTester.requires_root is False


# ---------------------------------------------------------------------------
# Risk constants
# ---------------------------------------------------------------------------

class TestRiskConstants:

    def test_action_baselines_exist(self):
        assert len(ACTION_RISK_BASELINE) > 10

    def test_passive_actions_are_low(self):
        for action in ["passive_sniff", "dns_logging", "pcap_capture"]:
            assert ACTION_RISK_BASELINE[action] == RISK_LOW

    def test_active_actions_are_high_or_critical(self):
        for action in ["responder", "ntlm_relay", "mitmproxy_ssl_intercept"]:
            assert ACTION_RISK_BASELINE[action] in (RISK_HIGH, RISK_CRITICAL)

    def test_caplet_actions_defined(self):
        assert "net.sniff" in CAPLET_ACTION_RISK
        assert "arp.spoof" in CAPLET_ACTION_RISK
        assert "https.proxy" in CAPLET_ACTION_RISK


# ---------------------------------------------------------------------------
# Rule loading
# ---------------------------------------------------------------------------

class TestRuleLoading:

    def test_no_rules_dir(self, ids):
        assert ids._rules == []

    def test_rules_loaded_from_file(self, ids_with_rules):
        assert len(ids_with_rules._rules) == 4

    def test_comments_skipped(self, ids_with_rules):
        sids = [r["sid"] for r in ids_with_rules._rules]
        assert all(s.isdigit() for s in sids)

    def test_parsed_rule_structure(self, ids_with_rules):
        rule = ids_with_rules._rules[0]
        assert "sid" in rule
        assert "msg" in rule
        assert "raw" in rule
        assert rule["sid"] == "1000001"


# ---------------------------------------------------------------------------
# Rule parsing
# ---------------------------------------------------------------------------

class TestRuleParsing:

    def test_parse_valid_rule(self):
        line = 'alert tcp any any -> any any (msg:"Test Rule"; sid:12345; classtype:trojan-activity;)'
        result = IDSTester._parse_rule(line)
        assert result is not None
        assert result["sid"] == "12345"
        assert result["msg"] == "Test Rule"
        assert result["classtype"] == "trojan-activity"

    def test_parse_rule_without_sid_returns_none(self):
        result = IDSTester._parse_rule("alert tcp any any -> any any (msg:\"No SID\";)")
        assert result is None

    def test_parse_rule_without_msg(self):
        result = IDSTester._parse_rule("alert tcp any any -> any any (sid:99999;)")
        assert result is not None
        assert result["msg"] == ""


# ---------------------------------------------------------------------------
# Risk assessment (baseline only, no rules)
# ---------------------------------------------------------------------------

class TestBaselineRiskAssessment:

    def test_passive_action_low_risk(self, ids):
        result = ids.assess_risk("passive_sniff")
        assert result["risk_level"] == RISK_LOW

    def test_active_action_high_risk(self, ids):
        result = ids.assess_risk("responder")
        assert result["risk_level"] == RISK_HIGH

    def test_critical_action(self, ids):
        result = ids.assess_risk("mitmproxy_ssl_intercept")
        assert result["risk_level"] == RISK_CRITICAL

    def test_unknown_action_medium_risk(self, ids):
        result = ids.assess_risk("completely_unknown_action")
        assert result["risk_level"] == RISK_MEDIUM

    def test_assessment_stored(self, ids):
        ids.assess_risk("arp_spoof")
        assert len(ids._assessments) == 1
        assert ids._last_assessment.action == "arp_spoof"


# ---------------------------------------------------------------------------
# Risk escalation with rules
# ---------------------------------------------------------------------------

class TestRiskEscalation:

    def test_responder_escalates_with_matching_rules(self, ids_with_rules):
        result = ids_with_rules.assess_risk("responder")
        assert result["risk_level"] in (RISK_HIGH, RISK_CRITICAL)
        assert len(result["matching_sids"]) > 0

    def test_matching_sids_capped_at_20(self, mock_bus, mock_state, tmp_path):
        rules_dir = tmp_path / "rules"
        rules_dir.mkdir()
        lines = "\n".join(
            f'alert tcp any any -> any any (msg:"ARP rule {i}"; sid:{2000+i};)'
            for i in range(30)
        )
        (rules_dir / "arp.rules").write_text(lines)

        config = {"rules_dir": str(rules_dir)}
        mod = IDSTester(mock_bus, mock_state, config)
        mod.start()

        result = mod.assess_risk("arp_spoof")
        assert len(result["matching_sids"]) <= 20


# ---------------------------------------------------------------------------
# Recommendation text
# ---------------------------------------------------------------------------

class TestRecommendation:

    def test_critical_recommendation(self, ids):
        result = ids.assess_risk("mitmproxy_ssl_intercept")
        assert "CRITICAL" in result["recommendation"] or result["risk_level"] == RISK_CRITICAL

    def test_low_recommendation(self, ids):
        result = ids.assess_risk("passive_sniff")
        assert "LOW" in result["recommendation"]

    def test_recommendation_includes_action_name(self, ids):
        result = ids.assess_risk("arp_spoof")
        assert "arp_spoof" in result["recommendation"]


# ---------------------------------------------------------------------------
# Keyword mapping
# ---------------------------------------------------------------------------

class TestKeywordMapping:

    def test_responder_keywords(self):
        keywords = IDSTester._action_to_keywords("responder")
        assert "llmnr" in keywords
        assert "nbns" in keywords

    def test_arp_spoof_keywords(self):
        keywords = IDSTester._action_to_keywords("arp_spoof")
        assert "arp" in keywords

    def test_unknown_action_uses_name(self):
        keywords = IDSTester._action_to_keywords("custom_action")
        assert "custom action" in keywords


# ---------------------------------------------------------------------------
# Caplet assessment
# ---------------------------------------------------------------------------

class TestCapletAssessment:

    def test_assess_caplet_detects_actions(self, ids):
        caplet = (
            "# Bettercap caplet\n"
            "net.sniff on\n"
            "arp.spoof on\n"
        )
        results = ids.assess_caplet(caplet)
        assert len(results) == 2

    def test_assess_caplet_skips_comments(self, ids):
        caplet = "# net.sniff on\n"
        results = ids.assess_caplet(caplet)
        assert len(results) == 0

    def test_assess_caplet_includes_line(self, ids):
        caplet = "arp.spoof on\n"
        results = ids.assess_caplet(caplet)
        assert results[0]["caplet_line"] == "arp.spoof on"


# ---------------------------------------------------------------------------
# Preflight check
# ---------------------------------------------------------------------------

class TestPreflightCheck:

    def test_low_risk_go(self, ids):
        result = ids.preflight_check("packet_capture", ["passive_sniff", "pcap_capture"])
        assert result["go_nogo"] == "GO"
        assert result["overall_risk"] == RISK_LOW

    def test_critical_risk_nogo(self, ids):
        result = ids.preflight_check("mitmproxy", ["mitmproxy_ssl_intercept"])
        assert result["go_nogo"] == "NO-GO"
        assert result["overall_risk"] == RISK_CRITICAL

    def test_high_risk_caution(self, ids):
        result = ids.preflight_check("responder_mgr", ["responder"])
        assert result["go_nogo"] == "CAUTION"
        assert result["overall_risk"] == RISK_HIGH

    def test_preflight_returns_per_action_risks(self, ids):
        result = ids.preflight_check("test_module", ["passive_sniff", "responder"])
        assert len(result["action_risks"]) == 2

    def test_preflight_overall_is_max(self, ids):
        result = ids.preflight_check("mixed", ["passive_sniff", "mitmproxy_ssl_intercept"])
        assert result["overall_risk"] == RISK_CRITICAL


# ---------------------------------------------------------------------------
# RiskAssessment dataclass
# ---------------------------------------------------------------------------

class TestRiskAssessmentDataclass:

    def test_to_dict(self):
        ra = RiskAssessment(
            action="test",
            risk_level=RISK_LOW,
            matching_sids=["100"],
            matching_rules=["Test rule"],
            recommendation="Safe",
        )
        d = ra.to_dict()
        assert d["action"] == "test"
        assert d["risk_level"] == RISK_LOW
        assert d["matching_sids"] == ["100"]


# ---------------------------------------------------------------------------
# Status
# ---------------------------------------------------------------------------

class TestStatus:

    def test_status_running(self, ids):
        s = ids.status()
        assert s["running"] is True
        assert s["rules_loaded"] == 0
        assert s["total_assessments"] == 0

    def test_status_after_assessment(self, ids):
        ids.assess_risk("arp_spoof")
        s = ids.status()
        assert s["total_assessments"] == 1
        assert s["last_assessment_action"] == "arp_spoof"

    def test_status_with_rules(self, ids_with_rules):
        s = ids_with_rules.status()
        assert s["rules_loaded"] == 4

    def test_configure_reloads_rules(self, ids, tmp_path):
        new_dir = tmp_path / "new_rules"
        new_dir.mkdir()
        (new_dir / "test.rules").write_text(
            'alert tcp any any -> any any (msg:"New"; sid:99999;)\n'
        )
        ids.configure({"rules_dir": str(new_dir)})
        assert len(ids._rules) == 1


# ---------------------------------------------------------------------------
# Assessment history
# ---------------------------------------------------------------------------

class TestAssessmentHistory:

    def test_history_capped_at_100(self, ids):
        for i in range(110):
            ids.assess_risk("arp_spoof")
        assert len(ids._assessments) == 100
