"""E2E: logs + exec + open lifecycle with real verification.

Deploys an app, retrieves real logs (verifying content), execs commands
inside pods (verifying output), tests follow mode, and tests URL resolution.

Run with: pytest tests/e2e/test_services_logs_exec.py -v -s
"""

from __future__ import annotations

import threading
from typing import Any

import pytest

from kuberoku.factory import KuberokuFactory
from kuberoku.models import LogLine
from kuberoku.sdk.ingress import detect_ingress_controller
from tests.e2e.helpers import (
    cleanup_namespace_fixture,
    exec_in_pod_safe,
    find_running_pod,
    make_real_factory,
    poll_until,
    skip_if_no_cluster,
    wait_for_pods_ready,
)


@pytest.fixture(scope="module")
def real_factory() -> KuberokuFactory:
    return make_real_factory("e2e-logexec")


@pytest.fixture(scope="module", autouse=True)
def cleanup_namespace(real_factory: KuberokuFactory) -> Any:
    yield
    cleanup_namespace_fixture(real_factory)


def test_00_skip_if_no_cluster(real_factory: KuberokuFactory) -> None:
    skip_if_no_cluster()


def test_01_create_and_deploy(real_factory: KuberokuFactory) -> None:
    real_factory.apps.create("logtest", on_step=lambda s: None)
    real_factory.deploy.deploy(
        "logtest",
        image="nginx:1.27-alpine",
        ports=[{"containerPort": 80, "protocol": "TCP"}],
        wait=False,
    )
    print(f"\n  Deployed in ns: {real_factory.namespace}")


def test_02_wait_pods(real_factory: KuberokuFactory) -> None:
    wait_for_pods_ready(real_factory, "logtest")
    print("\n  Pods ready")


# ── Logs ─────────────────────────────────────────────────────────────


def test_03_generate_traffic(real_factory: KuberokuFactory) -> None:
    """Generate HTTP requests so nginx writes access logs."""
    pod_name = find_running_pod(real_factory, "logtest")
    assert pod_name is not None
    for _ in range(3):
        exec_in_pod_safe(
            real_factory,
            pod_name,
            ["sh", "-c", "wget -qO- http://localhost:80/ > /dev/null 2>&1 || true"],
        )
    print("\n  Generated 3 HTTP requests for log content")


def test_04_get_logs_has_content(real_factory: KuberokuFactory) -> None:
    """get_logs() should return lines with actual nginx log content."""

    def _has_meaningful_logs() -> list[LogLine]:
        lines = real_factory.services.get_logs("logtest", num=50)
        non_empty = [ll for ll in lines if ll.message.strip()]
        if not non_empty:
            return []
        return lines

    log_lines = poll_until(
        fn=_has_meaningful_logs,
        predicate=lambda lines: len(lines) >= 1,
        timeout=10,
        interval=0.5,
        message="No log lines appeared after generating traffic",
    )

    # Verify nginx access log pattern (GET request)
    has_get = any("GET" in ll.message or "nginx" in ll.message.lower() for ll in log_lines)
    # Also check for startup log or error log
    has_content = has_get or any(len(ll.message) > 10 for ll in log_lines)
    preview = [ll.message[:50] for ll in log_lines[:5]]
    assert has_content, f"No meaningful log content found: {preview}"

    non_empty = [ll for ll in log_lines if ll.message.strip()]
    print(f"\n  Got {len(log_lines)} log lines, {len(non_empty)} non-empty")
    for line in log_lines[:3]:
        print(f"  {line.dyno}: {line.message[:80]}")


def test_05_logs_have_correct_structure(real_factory: KuberokuFactory) -> None:
    """Each log line should have dyno name, source, and timestamp."""
    log_lines = real_factory.services.get_logs("logtest", num=10)
    for ll in log_lines:
        assert ll.dyno, f"Log line missing dyno: {ll}"
        assert ll.source == "app", f"Log line source should be 'app', got '{ll.source}'"
        assert ll.timestamp is not None, f"Log line missing timestamp: {ll}"
    print(f"\n  All {len(log_lines)} log lines have correct structure")


def test_06_logs_follow_receives_new_lines(real_factory: KuberokuFactory) -> None:
    """Follow mode should deliver new log lines when traffic is generated."""
    collected: list[LogLine] = []
    stop_event = threading.Event()

    def on_line(line: LogLine) -> None:
        collected.append(line)

    # Start following in a background thread
    follow_thread = threading.Thread(
        target=real_factory.services.stream_logs,
        kwargs={
            "app": "logtest",
            "num": 5,
            "on_line": on_line,
            "stop_event": stop_event,
        },
        daemon=True,
    )
    follow_thread.start()

    # Poll until follow connects (initial tail lines arrive)
    poll_until(
        fn=lambda: len(collected),
        predicate=lambda n: n >= 1,
        timeout=10,
        interval=0.2,
        message="Follow mode did not deliver initial tail lines",
    )

    # Generate new traffic
    initial_count = len(collected)
    pod_name = find_running_pod(real_factory, "logtest")
    assert pod_name is not None
    for _ in range(3):
        exec_in_pod_safe(
            real_factory,
            pod_name,
            ["sh", "-c", "wget -qO- http://localhost:80/ > /dev/null 2>&1 || true"],
        )

    # Poll until new log lines arrive
    poll_until(
        fn=lambda: len(collected),
        predicate=lambda n: n > initial_count,
        timeout=10,
        interval=0.2,
        message="Follow mode did not deliver new lines after traffic",
    )

    stop_event.set()
    follow_thread.join(timeout=5)

    print(f"\n  Follow mode received {len(collected)} lines")
    for line in collected[:3]:
        print(f"  {line.dyno}: {line.message[:80]}")


# ── Exec ─────────────────────────────────────────────────────────────


def test_07_exec_echo(real_factory: KuberokuFactory) -> None:
    """exec 'echo hello' should return 'hello'."""
    result = real_factory.services.exec_interactive(
        "logtest",
        ["echo", "hello-from-exec"],
        tty=False,
    )
    assert "hello-from-exec" in result.output
    print(f"\n  exec output: {result.output.strip()}")


def test_08_exec_env(real_factory: KuberokuFactory) -> None:
    """exec 'env' should contain KUBERNETES vars."""
    result = real_factory.services.exec_interactive(
        "logtest",
        ["env"],
        tty=False,
    )
    assert "KUBERNETES" in result.output
    print(f"\n  env contains KUBERNETES_* vars")


def test_09_exec_file_operations(real_factory: KuberokuFactory) -> None:
    """exec can write and read files inside the container."""
    # Write a file
    result_write = real_factory.services.exec_interactive(
        "logtest",
        ["sh", "-c", "echo 'test-data-12345' > /tmp/e2e-test.txt && echo WRITE_OK"],
        tty=False,
    )
    assert "WRITE_OK" in result_write.output

    # Read it back
    result_read = real_factory.services.exec_interactive(
        "logtest",
        ["cat", "/tmp/e2e-test.txt"],
        tty=False,
    )
    assert "test-data-12345" in result_read.output
    print(f"\n  Write+read file in container: OK")


def test_10_exec_detached(real_factory: KuberokuFactory) -> None:
    """exec_detached creates a Job and returns its name."""
    job = real_factory.services.exec_detached(
        "logtest",
        ["echo", "background-job"],
    )
    assert job.job_name != ""
    print(f"\n  Detached job created: {job.job_name}")

    # Verify the Job object exists in K8s
    try:
        real_factory.k8s.get_job(real_factory.namespace, job.job_name)
        print(f"  Job object verified in cluster")
    except Exception as e:
        print(f"  Job lookup failed: {e}")


# ── services:open URL resolution ─────────────────────────────────────


def test_11_open_url_fallback(real_factory: KuberokuFactory) -> None:
    """services:open should resolve to localhost fallback (no ingress/gateway)."""
    url = real_factory.services.open_url("logtest")
    # With no ingress/gateway/LB, should fall back to localhost:80
    assert url.startswith("http"), f"Expected http URL, got: {url}"
    assert "80" in url or "localhost" in url, f"Expected port 80 or localhost in URL: {url}"
    print(f"\n  open_url (no ingress): {url}")


def test_12_open_url_with_domain(real_factory: KuberokuFactory) -> None:
    """If a domain is added, open_url should return the domain URL."""
    # Only test if ingress controller is available
    controller = detect_ingress_controller(real_factory.k8s, real_factory.namespace)
    if controller is None:
        # Skip domain test, verify fallback still works
        url = real_factory.services.open_url("logtest")
        assert url.startswith("http")
        print(f"\n  No ingress controller, fallback URL: {url}")
        return

    real_factory.domains.add("logtest", "logtest.e2e.local", no_tls=True)
    url = real_factory.services.open_url("logtest")
    assert "logtest.e2e.local" in url, f"Expected domain in URL: {url}"
    print(f"\n  open_url with domain: {url}")

    # Clean up the domain
    real_factory.domains.remove("logtest", "logtest.e2e.local")


# ── Cleanup ──────────────────────────────────────────────────────────


def test_13_cleanup(real_factory: KuberokuFactory) -> None:
    real_factory.apps.destroy("logtest")
    apps = real_factory.apps.list()
    assert not any(a.name == "logtest" for a in apps)
    print("\n  Cleanup complete")
