"""
Integration tests for the NIDS system.
Runs directly on the VM host against the live, running Docker Compose stack
(capture-service, api-service, dashboard) to verify the components talk to
each other correctly across process/container boundaries.

Usage:
    docker compose up -d
    python3 tests/integration_test.py
"""

import json
import os
import sqlite3
import sys
import time
import urllib.request

PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(PROJECT_ROOT, "capture-service"))

import db_writer  # noqa: E402

# Host-side path to the same SQLite file the containers see via the shared volume
HOST_DB_PATH = os.path.join(PROJECT_ROOT, "shared_data", "detections.db")
db_writer.DB_PATH = HOST_DB_PATH

API_URL = "http://localhost:8000"
DASHBOARD_URL = "http://localhost:8501"

# Unique, reserved test IP (TEST-NET-3, RFC 5737) so it never collides with
# real captured traffic and is easy to find/clean up afterwards.
TEST_SRC_IP = "203.0.113.99"

passed = 0
failed = 0


def check(condition, description):
    global passed, failed
    if condition:
        print(f"  PASSED  {description}")
        passed += 1
    else:
        print(f"  FAILED  {description}")
        failed += 1


def http_get_json(url):
    with urllib.request.urlopen(url, timeout=5) as resp:
        return json.loads(resp.read().decode())


def http_get_status(url):
    try:
        with urllib.request.urlopen(url, timeout=5) as resp:
            return resp.status
    except Exception as e:
        return str(e)


def cleanup_test_rows():
    conn = sqlite3.connect(HOST_DB_PATH)
    cursor = conn.cursor()
    cursor.execute("DELETE FROM detections WHERE src_ip = ?", (TEST_SRC_IP,))
    conn.commit()
    conn.close()


# ---------------------------------------------------------------------------
print("=" * 70)
print("Test 1: db_writer -> SQLite (write path)")
print("=" * 70)

db_writer.init_db()
db_writer.log_detection(
    src_ip=TEST_SRC_IP,
    dst_ip="10.0.0.1",
    dst_port=443,
    stage="INTEGRATION_TEST",
    decision="BENIGN",
    reconstruction_error=0.0123,
    threshold=0.0155,
    processing_latency_ms=12.5,
    flow_duration_sec=1.5,
)

conn = sqlite3.connect(HOST_DB_PATH)
cursor = conn.cursor()
cursor.execute(
    "SELECT decision, reconstruction_error, threshold FROM detections "
    "WHERE src_ip = ? ORDER BY id DESC LIMIT 1",
    (TEST_SRC_IP,),
)
row = cursor.fetchone()
conn.close()

check(row is not None, "test row exists in detections.db after log_detection()")
if row is not None:
    check(row[0] == "BENIGN", "decision stored correctly")
    check(abs(row[1] - 0.0123) < 1e-6, "reconstruction_error stored correctly")
    check(abs(row[2] - 0.0155) < 1e-6, "threshold stored correctly")

# ---------------------------------------------------------------------------
print()
print("=" * 70)
print("Test 2: api-service -> database (read path)")
print("=" * 70)

try:
    detections = http_get_json(f"{API_URL}/detections")
    check(isinstance(detections, list), "GET /detections returns a JSON list")

    test_rows = [d for d in detections if d.get("src_ip") == TEST_SRC_IP]
    check(len(test_rows) >= 1, "test row written in Test 1 is visible via the API")
    if test_rows:
        check(test_rows[-1]["decision"] == "BENIGN", "API-returned decision matches what was written")

    active = http_get_json(f"{API_URL}/active_flows")
    check("active_flows_count" in active, "GET /active_flows returns expected shape")
except Exception as e:
    check(False, f"api-service reachable and responding ({e})")

# ---------------------------------------------------------------------------
print()
print("=" * 70)
print("Test 3: dashboard -> api-service connectivity")
print("=" * 70)

status = http_get_status(DASHBOARD_URL)
check(status == 200, f"dashboard is reachable on {DASHBOARD_URL} (status={status})")

# ---------------------------------------------------------------------------
cleanup_test_rows()

print()
print("=" * 70)
print(f"SUMMARY: {passed} passed, {failed} failed")
print("=" * 70)

sys.exit(0 if failed == 0 else 1)
