"""Tests para nmap_scan (wrapper nmap, estilo dict sin excepciones).

SIN red: nunca ejecuta nmap real. subprocess.run se monkeypatchea para que el
guard y el parseo de XML se prueben de forma determinista y offline.
"""

import os
import subprocess
import sys
import tempfile

sys.path.insert(0, os.path.dirname(__file__))

from nmap_scan import _parse_xml, _target_is_private, nmap_scan

# XML fixture minimo de nmap: un host up con el puerto 22/tcp open (ssh).
NMAP_XML = """\
<?xml version="1.0" encoding="UTF-8"?>
<nmaprun scanner="nmap">
  <host>
    <status state="up" reason="syn-ack"/>
    <address addr="192.168.1.10" addrtype="ipv4"/>
    <ports>
      <port protocol="tcp" portid="22">
        <state state="open" reason="syn-ack"/>
        <service name="ssh" product="OpenSSH" version="8.9"/>
      </port>
      <port protocol="tcp" portid="80">
        <state state="closed" reason="reset"/>
        <service name="http"/>
      </port>
    </ports>
  </host>
</nmaprun>
"""


def _make_fake_run(write_xml: bool = True, returncode: int = 0):
    """Devuelve un fake de subprocess.run que escribe el XML fixture en la ruta
    que sigue a '-oX' en el comando y devuelve un CompletedProcess."""

    def fake_run(cmd, *args, **kwargs):
        if write_xml:
            idx = cmd.index("-oX")
            xml_path = cmd[idx + 1]
            with open(xml_path, "w", encoding="utf-8") as fh:
                fh.write(NMAP_XML)
        return subprocess.CompletedProcess(
            args=cmd, returncode=returncode, stdout="raw nmap output", stderr=""
        )

    return fake_run


def _fail_if_called(*args, **kwargs):
    """subprocess.run que falla el test si se invoca (el guard NO debe ejecutar nmap)."""
    raise AssertionError("subprocess.run no debe llamarse cuando el guard rechaza el target")


# --- 1. _parse_xml: golden ---------------------------------------------------

def test_parse_xml_extrae_puertos_abiertos_y_hosts_up():
    """Escribe el XML fixture a un tmp y comprueba el parseo."""
    fd, xml_path = tempfile.mkstemp(suffix=".xml")
    os.close(fd)
    try:
        with open(xml_path, "w", encoding="utf-8") as fh:
            fh.write(NMAP_XML)

        open_ports, hosts_up, host_status = _parse_xml(xml_path)

        assert hosts_up == ["192.168.1.10"]
        assert host_status == "up"
        # Solo el puerto 22 esta open; el 80 esta closed y se descarta.
        assert len(open_ports) == 1
        p = open_ports[0]
        assert p["port"] == 22
        assert p["proto"] == "tcp"
        assert p["state"] == "open"
        assert p["service"] == "ssh"
        assert p["product"] == "OpenSSH"
        assert p["version"] == "8.9"
    finally:
        os.remove(xml_path)


# --- 2. Guard error path: publico sin confirm no ejecuta nmap ----------------

def test_guard_publico_sin_confirm_rechaza_y_no_ejecuta(monkeypatch):
    """8.8.8.8 (publico) sin confirm -> error + needs_confirm, sin tocar subprocess."""
    monkeypatch.setattr(subprocess, "run", _fail_if_called)

    result = nmap_scan("8.8.8.8")

    assert result["status"] == "error"
    assert result["needs_confirm"] is True
    assert "8.8.8.8" in result["error"]


# --- 3. Guard privado OK: procede y parsea -----------------------------------

def test_guard_privado_procede_y_parsea(monkeypatch):
    """192.168.1.10 (privado) sin confirm -> procede; XML parseado."""
    monkeypatch.setattr(subprocess, "run", _make_fake_run())

    result = nmap_scan("192.168.1.10")

    assert result["status"] == "ok"
    assert result["hosts_up"] == ["192.168.1.10"]
    assert len(result["open_ports"]) == 1
    assert result["open_ports"][0]["port"] == 22
    assert result["raw"] == "raw nmap output"


# --- 4. Guard confirm=True sobre publico procede -----------------------------

def test_guard_confirm_true_sobre_publico_procede(monkeypatch):
    """8.8.8.8 (publico) con confirm=True -> procede."""
    monkeypatch.setattr(subprocess, "run", _make_fake_run())

    result = nmap_scan("8.8.8.8", confirm=True)

    assert result["status"] == "ok"
    assert len(result["open_ports"]) == 1


# --- 5. Guard allowlist: target autorizado procede ---------------------------

def test_guard_allowlist_procede(monkeypatch):
    """scanme.nmap.org en allowlist -> procede sin confirm."""
    monkeypatch.setattr(subprocess, "run", _make_fake_run())

    result = nmap_scan("scanme.nmap.org", allowlist=["scanme.nmap.org"])

    assert result["status"] == "ok"


# --- 6. Errores de validacion ------------------------------------------------

def test_perfil_invalido_devuelve_error(monkeypatch):
    """Un perfil no listado -> status error, sin ejecutar nmap."""
    monkeypatch.setattr(subprocess, "run", _fail_if_called)

    result = nmap_scan("192.168.1.10", profile="noexiste")

    assert result["status"] == "error"
    assert "invalido" in result["error"]


def test_target_vacio_devuelve_error(monkeypatch):
    """Target vacio -> status error, sin ejecutar nmap."""
    monkeypatch.setattr(subprocess, "run", _fail_if_called)

    result = nmap_scan("")

    assert result["status"] == "error"
    assert "vacio" in result["error"]


# --- 7. _target_is_private: clasificacion ------------------------------------

def test_target_is_private_clasifica():
    """Privados/local -> True; publico -> False; hostname publico -> None."""
    assert _target_is_private("10.0.0.1") is True
    assert _target_is_private("127.0.0.1") is True
    assert _target_is_private("192.168.0.0/24") is True
    assert _target_is_private("8.8.8.8") is False
    assert _target_is_private("localhost") is True
    assert _target_is_private("foo.local") is True
    assert _target_is_private("example.com") is None
