"""Tests for modules/active/responder_mgr — hash capture, dedup, config generation.

Validates hash parsing, deduplication, Responder.conf generation,
relay target exclusion, and status reporting without requiring
Responder binary, root, 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.active.responder_mgr import (
    ResponderManager, HASH_FILE_PATTERN, NTLMV2_REGEX, NTLMV1_REGEX,
)


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

@pytest.fixture
def responder(mock_bus, mock_state, tmp_path):
    """Return a ResponderManager with temp dirs and mocked externals."""
    config = {
        "responder_path": str(tmp_path / "Responder"),
        "interface": "eth0",
        "template_dir": str(tmp_path / "templates"),
    }
    resp_dir = tmp_path / "Responder"
    resp_dir.mkdir()
    (resp_dir / "logs").mkdir()
    (resp_dir / "Responder.py").touch()

    mod = ResponderManager(mock_bus, mock_state, config)
    mod.start()
    return mod


@pytest.fixture
def hash_log_dir(responder, tmp_path):
    """Return the Responder logs directory."""
    return tmp_path / "Responder" / "logs"


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

class TestResponderStructure:

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

    def test_module_attributes(self):
        assert ResponderManager.name == "responder_mgr"
        assert ResponderManager.module_type == "active"
        assert ResponderManager.requires_root is True


# ---------------------------------------------------------------------------
# Regex patterns
# ---------------------------------------------------------------------------

class TestHashRegex:

    def test_hash_file_pattern_matches_ntlmv2(self):
        assert HASH_FILE_PATTERN.search("SMB-NTLMv2-10.0.0.5.txt")

    def test_hash_file_pattern_matches_ntlmv1(self):
        assert HASH_FILE_PATTERN.search("HTTP-NTLMv1-10.0.0.5.txt")

    def test_hash_file_pattern_rejects_other(self):
        assert not HASH_FILE_PATTERN.search("Responder-Session.log")

    def test_ntlmv2_regex_parses_valid_hash(self):
        line = "admin::CORP:aabbccdd11223344:eeff00112233445566778899aabbccdd:0011223344556677"
        m = NTLMV2_REGEX.match(line)
        assert m is not None
        assert m.group("username") == "admin"
        assert m.group("domain") == "CORP"

    def test_ntlmv1_regex_parses_valid_hash(self):
        line = "user1::DOMAIN:aabbccdd11223344:eeff00112233445566778899aabbccdd:0011223344556677"
        m = NTLMV1_REGEX.match(line)
        assert m is not None
        assert m.group("username") == "user1"
        assert m.group("domain") == "DOMAIN"

    def test_ntlmv2_regex_rejects_garbage(self):
        assert NTLMV2_REGEX.match("not a hash line") is None


# ---------------------------------------------------------------------------
# Hash line processing
# ---------------------------------------------------------------------------

class TestHashProcessing:

    def test_process_ntlmv2_hash_line(self, responder):
        line = "admin::CORP:aabbccdd11223344:eeff00112233445566778899aabbccdd:0011223344556677"
        responder._process_hash_line(line, "SMB-NTLMv2-10.0.0.5.txt")

        hashes = responder.get_captured_hashes()
        assert len(hashes) == 1
        assert hashes[0]["username"] == "admin"
        assert hashes[0]["domain"] == "CORP"
        assert hashes[0]["hash_type"] == "ntlmv2"
        assert hashes[0]["hashcat_mode"] == 5600

    def test_process_ntlmv1_hash_line(self, responder):
        line = "user1::DOMAIN:aabb:ccdd:eeff"
        responder._process_hash_line(line, "HTTP-NTLMv1-10.0.0.5.txt")

        hashes = responder.get_captured_hashes()
        assert len(hashes) == 1
        assert hashes[0]["hash_type"] == "ntlmv1"
        assert hashes[0]["hashcat_mode"] == 5500

    def test_process_unparseable_line(self, responder):
        """Lines without :: still get processed with username='unknown'."""
        responder._process_hash_line("garbage", "SMB-NTLMv2-test.txt")
        hashes = responder.get_captured_hashes()
        assert len(hashes) == 1
        assert hashes[0]["username"] == "unknown"


# ---------------------------------------------------------------------------
# Hash file scanning
# ---------------------------------------------------------------------------

class TestHashFileScanning:

    def test_scan_hash_files_discovers_new_hashes(self, responder, hash_log_dir):
        hash_file = hash_log_dir / "SMB-NTLMv2-10.0.0.5.txt"
        hash_file.write_text(
            "admin::CORP:aa:bb:cc\n"
            "user2::CORP:dd:ee:ff\n"
        )
        responder._scan_hash_files()
        assert len(responder.get_captured_hashes()) == 2

    def test_scan_deduplicates_seen_hashes(self, responder, hash_log_dir):
        hash_file = hash_log_dir / "SMB-NTLMv2-10.0.0.5.txt"
        hash_file.write_text("admin::CORP:aa:bb:cc\n")

        responder._scan_hash_files()
        responder._scan_hash_files()

        assert len(responder.get_captured_hashes()) == 1

    def test_scan_ignores_empty_lines(self, responder, hash_log_dir):
        hash_file = hash_log_dir / "SMB-NTLMv2-10.0.0.5.txt"
        hash_file.write_text("\n\nadmin::CORP:aa:bb:cc\n\n")

        responder._scan_hash_files()
        assert len(responder.get_captured_hashes()) == 1


# ---------------------------------------------------------------------------
# Responder.conf generation
# ---------------------------------------------------------------------------

class TestConfGeneration:

    def test_fallback_conf_written(self, responder, tmp_path):
        responder._write_responder_conf()
        conf_path = tmp_path / "Responder" / "Responder.conf"
        assert conf_path.exists()
        content = conf_path.read_text()
        assert "SMB = On" in content
        assert "HTTP = On" in content

    def test_protocol_toggles_reflected(self, responder, tmp_path):
        responder._protocols["SMB"] = False
        responder._protocols["HTTP"] = False
        responder._write_responder_conf()

        conf_path = tmp_path / "Responder" / "Responder.conf"
        content = conf_path.read_text()
        assert "SMB = Off" in content
        assert "HTTP = Off" in content


# ---------------------------------------------------------------------------
# Relay target exclusion
# ---------------------------------------------------------------------------

class TestRelayExclusion:

    def test_set_relay_targets(self, responder):
        responder.set_relay_targets({"10.0.0.1", "10.0.0.2"})
        assert "10.0.0.1" in responder._relay_targets
        assert "10.0.0.2" in responder._relay_targets

    def test_set_relay_targets_replaces_previous(self, responder):
        responder.set_relay_targets({"10.0.0.1"})
        responder.set_relay_targets({"10.0.0.2"})
        assert "10.0.0.1" not in responder._relay_targets
        assert "10.0.0.2" in responder._relay_targets


# ---------------------------------------------------------------------------
# Configure
# ---------------------------------------------------------------------------

class TestConfigure:

    def test_configure_updates_interface(self, responder):
        responder.configure({"interface": "wlan0"})
        assert responder._iface == "wlan0"

    def test_configure_updates_relay_targets(self, responder):
        responder.configure({"relay_targets": ["10.0.0.5"]})
        assert "10.0.0.5" in responder._relay_targets

    def test_configure_updates_protocols(self, responder):
        responder.configure({"protocols": {"FTP": True, "SMB": False}})
        assert responder._protocols["FTP"] is True
        assert responder._protocols["SMB"] is False


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

class TestStatus:

    def test_status_when_running(self, responder):
        s = responder.status()
        assert s["running"] is True
        assert s["responder_running"] is False
        assert s["captured_hash_count"] == 0
        assert s["interface"] == "eth0"

    def test_status_reflects_captured_hashes(self, responder):
        responder._process_hash_line("admin::CORP:aa:bb:cc", "SMB-NTLMv2-test.txt")
        s = responder.status()
        assert s["captured_hash_count"] == 1

    def test_status_shows_relay_targets(self, responder):
        responder.set_relay_targets({"10.0.0.1"})
        s = responder.status()
        assert "10.0.0.1" in s["relay_targets"]
