"""
Simulación de escenarios de diagnóstico (sin red real).
Ejecutar: python -m tests.simulate
"""
from __future__ import annotations

import sys
import time
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))

from app.arp_watch import ArpChange, ArpWatcher
from app.events import EventLog
from app.models import Device, DeviceStatus, NetInfo, now_str
from app.monitor import MonitorEngine, TimelineEvent
from app.report import build_conclusion, generate_html_report


def test_ip_conflict(log: EventLog, arp: ArpWatcher) -> None:
    print("=== Simulación: IP duplicada / cambio de MAC ===")
    arp.record_observation("192.168.1.105", "AA:BB:CC:11:22:33")
    ch = arp.record_observation("192.168.1.105", "11:22:33:44:55:66")
    assert ch is not None and ch.kind == "conflict"
    log.critical(ch.ip, "IP CONFLICT", f"{ch.old_mac} → {ch.new_mac}")
    print(f"OK CONFLICTO: {ch.ip} {ch.old_mac} → {ch.new_mac}")


def test_offline_online(log: EventLog) -> None:
    print("=== Simulación: cámara desaparece y vuelve ===")
    ip = "192.168.1.44"
    log.error(ip, "OFFLINE", "14:32:17")
    time.sleep(0.05)
    log.info(ip, "ONLINE", "duración caída: 14 s")
    print("OK OFFLINE → ONLINE registrado")


def test_packet_loss(log: EventLog) -> None:
    print("=== Simulación: pérdida de paquetes ===")
    log.warning("192.168.1.48", "PACKET LOSS", "20%")
    d = Device(ip="192.168.1.48", status=DeviceStatus.DEGRADED.value, loss_pct=20, sent=100, recv=80)
    assert d.status == "DEGRADADO"
    print(f"OK DEGRADADO {d.ip} loss={d.loss_pct}%")


def test_progressive_drop(log: EventLog, mon: MonitorEngine) -> None:
    print("=== Simulación: múltiples cámaras cayendo progresivamente ===")
    base = time.time()
    ips = ["192.168.1.41", "192.168.1.42", "192.168.1.43", "192.168.1.44", "192.168.1.45"]
    for i, ip in enumerate(ips):
        te = TimelineEvent(base + i * 2, now_str(), ip, "offline", "")
        mon.timeline.append(te)
        mon._offline_burst.append(te)
        log.error(ip, "OFFLINE", f"+{i*2}s")
    mon._check_correlation()
    crit = [e for e in log.all(["CRITICAL"]) if e.event == "CORRELACION"]
    assert crit, "Se esperaba alerta de correlación"
    print(f"OK CORRELACIÓN: {crit[-1].details}")


def test_mac_change_event(log: EventLog) -> None:
    print("=== Simulación: cambio ARP ===")
    ch = ArpChange(
        ts=now_str(),
        ip="192.168.1.46",
        old_mac="AA:BB:CC:00:00:01",
        new_mac="AA:BB:CC:00:00:99",
        kind="conflict",
    )
    log.critical(ch.ip, "IP CONFLICT", f"{ch.old_mac} → {ch.new_mac}")
    print("OK evento ARP conflict registrado")


def test_report(log: EventLog, arp: ArpWatcher, mon: MonitorEngine) -> Path:
    print("=== Generar informe de ejemplo ===")
    net = NetInfo(
        adapter="Wi-Fi",
        ip="192.168.1.50",
        mask="255.255.255.0",
        gateway="192.168.1.1",
        cidr="192.168.1.0/24",
        dhcp_server="192.168.1.1",
        dns=["192.168.1.1"],
        is_admin=False,
    )
    devices = [
        Device(
            ip="192.168.1.1",
            mac="00:11:22:33:44:01",
            vendor="Askey",
            device_type="Router/AP",
            status="ONLINE",
            latency_ms=2,
        ),
        Device(
            ip="192.168.1.44",
            mac="3C:EA:4F:11:22:33",
            vendor="Dahua",
            device_type="Camara",
            status="ONLINE",
            latency_ms=18,
            loss_pct=2,
            sent=100,
            recv=98,
        ),
        Device(
            ip="192.168.1.105",
            mac="11:22:33:44:55:66",
            vendor="Dahua",
            device_type="Camara",
            status="CONFLICTO IP",
            notes="CONFLICTO IP detectado en simulación",
            sent=50,
            recv=40,
            loss_pct=20,
        ),
        Device(
            ip="192.168.1.48",
            mac="3C:EA:4F:AA:BB:CC",
            vendor="Dahua",
            device_type="Camara",
            status="OFFLINE",
            loss_pct=100,
            sent=30,
            recv=0,
        ),
    ]
    estado, conclusion = build_conclusion(devices, arp.conflicts(), list(mon.timeline), [])
    print(f"Estado: {estado}")
    print(f"Conclusión: {conclusion}")
    path = generate_html_report(
        net,
        devices,
        log,
        monitor=mon,
        arp=arp,
        dhcp_notes=["Simulación: informe de ejemplo"],
        title="Informe de ejemplo — NVR Net Diag (simulación)",
    )
    # copiar a reports/example_report.html
    example = ROOT / "reports" / "example_report.html"
    example.write_text(path.read_text(encoding="utf-8"), encoding="utf-8")
    print(f"OK informe: {path}")
    print(f"OK ejemplo: {example}")
    return path


def test_topology_partial_discovery() -> None:
    print("=== Simulación: NVR ve pocas cámaras (brecha multicast) ===")
    from app import topology as topo_mod

    devices = []
    # 2 NVR + 12 cámaras ONLINE
    for i, ip in enumerate(["192.168.1.10", "192.168.1.11"]):
        devices.append(
            Device(
                ip=ip,
                mac=f"AA:BB:CC:00:00:{i:02X}",
                vendor="PNI",
                device_type="NVR",
                status="ONLINE",
                latency_ms=1.0,
                ports={80: True, 37777: True},
            )
        )
    for i in range(12):
        ip = f"192.168.1.{40 + i}"
        devices.append(
            Device(
                ip=ip,
                mac=f"9C:A5:25:00:00:{i:02X}",
                vendor="Dahua",
                device_type="Camara",
                status="ONLINE",
                latency_ms=2.0 if i < 8 else 15.0,
                ports={80: True, 554: True},
            )
        )
    net = NetInfo(
        adapter="Ethernet",
        ip="192.168.1.50",
        mask="255.255.255.0",
        gateway="192.168.1.1",
        cidr="192.168.1.0/24",
        dhcp_server="192.168.1.1",
    )

    # Solo 5 cámaras anuncian discovery (como Buscar incompleto)
    fake_disc = {
        "ok": True,
        "ips": [f"192.168.1.{40 + i}" for i in range(5)],
        "raw_count": 5,
        "error": "",
        "note": "test",
        "local_bind": "192.168.1.50",
    }
    orig = topo_mod.ws_discovery_probe_both
    topo_mod.ws_discovery_probe_both = lambda **kwargs: fake_disc
    try:
        report = topo_mod.run_topology_analysis(
            devices, net, enrich=False, discovery_sec=0.1, probe_services=False
        )
    finally:
        topo_mod.ws_discovery_probe_both = orig

    text = report.as_text()
    assert len(report.inventory["Camaras"]) == 12
    assert len(report.inventory["NVR"]) == 2
    codes = {f.code for f in report.findings}
    assert "MULTICAST_GAP" in codes or "NVR_PARTIAL_DISCOVERY" in codes or "MULTI_NVR" in codes
    assert "192.168.1.10" in text
    print(f"OK topología: hallazgos={sorted(codes)}")
    print(f"   resumen: {report.summary[:120]}…")


def test_nvr_focus_report() -> None:
    print("=== Simulación: informe desde NVR con checklist manual ===")
    from app.nvr_focus_report import build_nvr_focus_report, generate_nvr_html_report

    nvr = Device(
        ip="192.168.1.10",
        mac="AA:BB:CC:00:00:01",
        vendor="PNI",
        device_type="NVR",
        status="ONLINE",
        ports={80: True, 37777: True},
    )
    devices = [nvr]
    for i in range(14):
        ip = f"192.168.1.{40 + i}"
        devices.append(
            Device(
                ip=ip,
                mac=f"9C:A5:25:00:01:{i:02X}",
                vendor="Dahua",
                device_type="Camara",
                status="ONLINE" if i < 12 else "OFFLINE",
                latency_ms=2.0 if i < 10 else 15.0,
                ports={80: True, 554: True},
            )
        )
    net = NetInfo(gateway="192.168.1.1", ip="192.168.1.50", cidr="192.168.1.0/24")
  # discovery solo 6 camaras
    discovery = {f"192.168.1.{40 + i}" for i in range(6)}
    report = build_nvr_focus_report(nvr, devices, net, discovery)
    text = report.as_text()
    add_manual = [c for c in report.checklist if c.reachable and not c.in_discovery]
    assert len(add_manual) >= 4
    assert all(len(c.manual_steps) >= 3 for c in add_manual)
    assert "Anadir manual" in text or "ANADIR MANUALMENTE" in text
    path = generate_nvr_html_report(report, net)
    assert path.exists()
    print(f"OK informe NVR: {len(add_manual)} IPs manual, HTML {path.name}")


def test_encoder_lag_rules() -> None:
    print("=== Simulación: reglas lag por config video ===")
    from app.onvif_rtsp import evaluate_encoder_lag

    flags, hints = evaluate_encoder_lag(
        [
            {
                "encoding": "H264",
                "width": 2880,
                "height": 1620,
                "fps": 25,
                "bitrate_kbps": 5120,
                "gov_length": 2,
            }
        ]
    )
    assert any("BITRATE_MUY_ALTO" in f for f in flags)
    assert any("RESOLUCION_ALTA" in f for f in flags)
    assert hints
    print(f"OK flags={flags}")


def test_fluency_scoring() -> None:
    print("=== Simulación: score fluidez WiFi vs cable ===")
    from app import fluency as flu

    # Mock burst ping: WiFi mala
    def fake_burst(ip, count=12):
        if ip.endswith(".44"):
            return 55.0, 12.0, 20.0, True
        return 2.0, 0.0, 1.0, True

    orig = flu._burst_ping_stats
    orig_vid = flu.probe_video_encoders
    flu._burst_ping_stats = fake_burst
    flu.probe_video_encoders = lambda ip, **kw: {
        "ok": True,
        "profiles": [
            {"encoding": "H264", "width": 1920, "height": 1080, "fps": 25, "bitrate_kbps": 4096, "gov_length": 25}
        ],
        "lag_flags": ["BITRATE_MUY_ALTO:4096kbps"],
        "manual_hints": ["Baje a 1536 kbps"],
        "error": "",
    }
    try:
        devices = [
            Device(ip="192.168.1.10", device_type="NVR", status="ONLINE", ports={37777: True}, vendor="PNI"),
            Device(ip="192.168.1.11", device_type="NVR", status="ONLINE", ports={37777: True}, vendor="PNI"),
            Device(
                ip="192.168.1.44",
                device_type="Camara",
                status="ONLINE",
                vendor="Dahua",
                latency_ms=20,
                ports={80: True, 554: True},
            ),
            Device(
                ip="192.168.1.60",
                device_type="Camara",
                status="ONLINE",
                vendor="Dahua",
                latency_ms=1,
                ports={80: True, 554: True},
            ),
            Device(ip="192.168.1.2", device_type="Router/AP", vendor="Cudy", status="ONLINE"),
        ]
        net = NetInfo(gateway="192.168.1.1", ip="192.168.1.50")
        report = flu.run_fluency_analysis(devices, net, probe_video=True, ping_count=3, max_video_probes=4)
    finally:
        flu._burst_ping_stats = orig
        flu.probe_video_encoders = orig_vid

    text = report.as_text()
    assert report.nvr_count >= 2
    assert "QUE HACER" in text or "PASOS GLOBALES" in text
    wifi = next(c for c in report.cams if c.ip.endswith(".44"))
    assert wifi.score < 80
    print(f"OK fluidez: wifi_score={wifi.score} summary={report.summary[:80]}…")


def test_conflict_intel_identifies_family_vs_camera() -> None:
    print("=== Simulación: conflicto camara vs dispositivo familia ===")
    from app.arp_watch import ArpWatcher
    from app.conflict_intel import analyze_conflicts
    from app.models import Device, NetInfo

    arp = ArpWatcher()
    arp.record_observation("192.168.1.44", "9C:A5:25:00:00:01")
    ch = arp.record_observation("192.168.1.44", "AA:BB:CC:11:22:33")
    assert ch is not None
    devices = [
        Device(
            ip="192.168.1.44",
            mac="9C:A5:25:00:00:01",
            vendor="Dahua",
            device_type="Camara",
            status="ONLINE",
            ports={80: True, 554: True},
        ),
        Device(
            ip="192.168.1.80",
            mac="AA:BB:CC:11:22:33",
            vendor="Samsung",
            device_type="Desconocido",
            hostname="SM-A546",
            status="ONLINE",
        ),
        Device(ip="192.168.1.1", mac="00:11:22:00:00:01", vendor="Askey", device_type="Router/AP"),
        Device(ip="192.168.1.3", mac="C8:3A:35:00:00:02", vendor="Tenda", device_type="Router/AP"),
    ]
    net = NetInfo(gateway="192.168.1.1", dhcp_server="192.168.1.3", ip="192.168.1.50")
    intel = analyze_conflicts(devices, net, arp, run_dhcp_probe=False)
    text = intel.as_text()
    assert intel.cases
    assert "Camara" in intel.cases[0].party_a.role or "Camara" in intel.cases[0].party_b.role
    assert intel.dhcp.tenda_suspect or intel.dhcp.dual
    assert "QUE HACER" in text
    assert len(intel.family_devices) >= 1
    print(f"OK conflicto: {intel.cases[0].party_a.role} vs {intel.cases[0].party_b.role}")
    print(f"   DHCP: {intel.dhcp.who_commands[:80]}")


def test_perfect_lan_capacity() -> None:
    print("=== Simulación: plan red perfecta / capacidad 8 NVR ===")
    from app.perfect_lan import build_perfect_lan_plan, _bandwidth

    all_main, live_sub, w_main, w_sub, l_main = _bandwidth(8, 10, 12)
    assert all_main > live_sub
    assert w_main is False  # 10 wifi * 4096 * 8 no cabe en 2.4
    assert w_sub is True or live_sub > 0
    devices = []
    for i in range(8):
        devices.append(
            Device(ip=f"192.168.1.{10+i}", device_type="NVR", vendor="PNI", ports={37777: True}, status="ONLINE")
        )
    for i in range(10):
        devices.append(
            Device(
                ip=f"192.168.1.{40+i}",
                device_type="Camara",
                vendor="Dahua",
                latency_ms=20,
                ports={554: True, 80: True},
                status="ONLINE",
            )
        )
    for i in range(12):
        devices.append(
            Device(
                ip=f"192.168.1.{70+i}",
                device_type="Camara",
                vendor="Dahua",
                latency_ms=1,
                ports={554: True, 80: True},
                status="ONLINE",
            )
        )
    devices.append(Device(ip="192.168.1.1", device_type="Router/AP", vendor="Askey"))
    devices.append(Device(ip="192.168.1.3", device_type="Router/AP", vendor="Tenda"))
    devices.append(Device(ip="192.168.1.4", device_type="Router/AP", vendor="Cudy"))
    net = NetInfo(gateway="192.168.1.1", dhcp_server="192.168.1.1", ip="192.168.1.50")
    report = build_perfect_lan_plan(devices, net, dhcp_dual=False, dhcp_server="192.168.1.1", gateway="192.168.1.1")
    text = report.as_text()
    assert report.n_nvr >= 8
    assert report.n_wifi >= 8
    assert "SUB" in text or "sub" in text.lower()
    assert report.steps
    print(f"OK plan: score={report.score} all_main={report.mbps_all_main:.0f}Mbps sub={report.mbps_live_sub:.0f}Mbps")


def test_ip816_visor_manual() -> None:
    print("=== Simulación: test visores IP816 ===")
    from app import ip816_visor as mod
    from app.ip816_visor import VisorCheck

    def fake_probe(nvr, cam_ips, gw):
        return VisorCheck(
            ip=nvr.ip,
            mac=nvr.mac,
            ok_ping=True,
            latency_ms=2.0,
            ports={80: True, 37777: True},
            same_subnet=True,
            likely_wifi=False,
            score=75,
            issues=["Recordatorio Extra/Sub"],
            manual=["Encode: Extra stream"],
        )

    orig = mod._probe_nvr
    mod._probe_nvr = fake_probe
    try:
        devices = [
            Device(ip="192.168.1.10", device_type="NVR", vendor="PNI", ports={37777: True}),
        ]
        for i in range(7):
            devices.append(
                Device(ip=f"192.168.1.{11+i}", device_type="NVR", vendor="PNI", ports={37777: True})
            )
        for i in range(16):
            devices.append(Device(ip=f"192.168.1.{40+i}", device_type="Camara", ports={554: True}))
        net = NetInfo(gateway="192.168.1.1")
        report = mod.run_ip816_visor_test(devices, net, "192.168.1.10")
    finally:
        mod._probe_nvr = orig
    text = report.as_text()
    assert len(report.visors) == 7
    assert "Extra" in text or "SUB" in text or "Sub" in text
    assert "16" in text
    print(f"OK IP816: {len(report.visors)} visores, principal={report.principal_ip}")


def test_caudal_12_16() -> None:
    print("=== Simulación: caudal 12-16 / aplicar encode ===")
    from app import camera_caudal as cc
    from app.camera_caudal import budget_line, caudal_math_text, describe_kv_encode, pick_profile

    _, ok512, _ = budget_line(16, 512, 1)
    _, ok4k, _ = budget_line(16, 4096, 1)
    assert ok512, "16 x Extra 512 debe caber en 100M"
    assert not ok4k, "16 x Main 4096 no debe caber holgado"
    total7, _, _ = budget_line(10, 1536, 7)
    assert total7 > 25
    math = caudal_math_text()
    assert "12" in math and "16" in math
    assert "512" in math

    kv = {
        "table.Encode[0].MainFormat[0].Video.Width": "2560",
        "table.Encode[0].MainFormat[0].Video.Height": "1920",
        "table.Encode[0].MainFormat[0].Video.FPS": "20",
        "table.Encode[0].MainFormat[0].Video.BitRate": "4096",
        "table.Encode[0].MainFormat[0].Video.BitRateControl": "VBR",
        "table.Encode[0].MainFormat[0].Video.Compression": "H.265",
        "table.Encode[0].ExtraFormat[0].Video.Width": "704",
        "table.Encode[0].ExtraFormat[0].Video.Height": "576",
        "table.Encode[0].ExtraFormat[0].Video.FPS": "15",
        "table.Encode[0].ExtraFormat[0].Video.BitRate": "1024",
    }
    desc = describe_kv_encode(kv)
    assert "2560x1920" in desc
    assert "4096" in desc
    pairs = cc._set_pairs("visor_16ch", kv, True)
    d = dict(pairs)
    assert d["Encode[0].MainFormat[0].Video.BitRate"] == "1536"
    assert d["Encode[0].ExtraFormat[0].Video.BitRate"] == "512"
    assert d["Encode[0].MainFormat[0].Video.BitRateControl"] == "CBR"
    assert d["Encode[0].MainFormat[0].Video.Compression"] == "H.265"

    wifi_cam = Device(ip="192.168.1.40", device_type="Camara", latency_ms=22)
    poe_cam = Device(ip="192.168.1.70", device_type="Camara", latency_ms=1)
    assert pick_profile([wifi_cam, wifi_cam, wifi_cam]) == "visor_wifi"
    assert pick_profile([poe_cam, poe_cam, poe_cam]) == "visor_16ch"

    secret = "S3cretPass-NUNCA"
    orig_get = cc.dahua_cgi_get
    orig_set = cc.dahua_cgi_set
    orig_onvif = cc.probe_video_encoders

    def fake_get(ip, user, password, timeout=6.0):
        return True, "ok", kv

    def fake_set(ip, user, password, profile_key, kv=None, also_resolution=True, timeout=8.0):
        return True, "OK"

    cc.dahua_cgi_get = fake_get
    cc.dahua_cgi_set = fake_set
    cc.probe_video_encoders = lambda *a, **k: {"ok": False, "profiles": [], "error": "no"}
    try:
        cams = [Device(ip="192.168.1.40", device_type="Camara")]
        sim = cc.apply_caudal(cams, "visor_16ch", "admin", secret, dry_run=True)
        assert "SIMULACION" in sim
        assert "1536" in sim
        assert secret not in sim
        applied = cc.apply_caudal(cams, "visor_16ch", "admin", secret, dry_run=False)
        assert "APLICAR" in applied
        assert "[OK]" in applied
        assert secret not in applied
        assert "CGI Dahua" in applied
        xml = cc._wrap_set_encoder('<tt:VideoEncoderConfiguration token="Ve0"><tt:Width>1920</tt:Width></tt:VideoEncoderConfiguration>')
        assert "SetVideoEncoderConfiguration" in xml
        assert "trt:Configuration" in xml
        patched = cc._patch_limits("<Width>1920</Width><BitrateLimit>4096</BitrateLimit>", 1280, 720, 12, 1536, 12)
        assert "<BitrateLimit>1536</BitrateLimit>" in patched
    finally:
        cc.dahua_cgi_get = orig_get
        cc.dahua_cgi_set = orig_set
        cc.probe_video_encoders = orig_onvif
    print("OK caudal: 16x512 cabe, 16x4096 no, sim+apply CGI, clave no filtrada")


def test_site_and_switch_l2() -> None:
    print("=== Simulación: inventario NVR + guia switches 7 cams ===")
    from app.site_inventory import enrich_device, inventory_text, label_for, principal_hint_ip
    from app.switch_l2 import switch_l2_guide_text

    assert principal_hint_ip() == "192.168.1.62"
    assert "Salon" in label_for("192.168.1.62") or "NVR1" in label_for("192.168.1.62")
    d = Device(ip="192.168.1.64", device_type="Desconocido")
    assert enrich_device(d) is True
    assert d.device_type == "NVR"
    assert "Salon" in (d.hostname or "") or "NVR2" in (d.hostname or "")
    inv = inventory_text()
    assert "192.168.1.76" in inv and "192.168.1.57" in inv
    text = switch_l2_guide_text(
        [Device(ip="192.168.1.62", device_type="NVR", status="ONLINE"),
         Device(ip="192.168.1.2", vendor="D-Link", status="ONLINE")],
        NetInfo(gateway="192.168.1.1", dhcp_server="192.168.1.1"),
    )
    assert "VLAN" in text and "OFF" in text
    assert "NO configure nada especial en Movistar" in text or "NO configure" in text
    assert "192.168.1.62" in text
    assert "FS1018" in text or "Cudy" in text
    print("OK inventario + switches L2")


def main() -> int:
    log = EventLog()
    arp = ArpWatcher()
    mon = MonitorEngine(event_log=log, arp=arp)

    test_ip_conflict(log, arp)
    test_offline_online(log)
    test_packet_loss(log)
    test_mac_change_event(log)
    test_progressive_drop(log, mon)
    test_topology_partial_discovery()
    test_nvr_focus_report()
    test_encoder_lag_rules()
    test_fluency_scoring()
    test_conflict_intel_identifies_family_vs_camera()
    test_perfect_lan_capacity()
    test_ip816_visor_manual()
    test_caudal_12_16()
    test_site_and_switch_l2()
    test_report(log, arp, mon)

    print("\n=== TODAS LAS SIMULACIONES OK ===")
    log.close()
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
