import os
import signal
import subprocess
import sys
import textwrap
import time

import pytest


def _run_in_subprocess(
    code: str, *, timeout_s: float = 5.0
) -> subprocess.CompletedProcess[str]:
    """Run a tiny python program in a fresh process and capture output."""
    return subprocess.run(
        [sys.executable, "-c", code],
        text=True,
        capture_output=True,
        timeout=timeout_s,
        check=False,
    )


@pytest.mark.unit
def test_shutdown_handler_runs_on_sigterm() -> None:
    code = textwrap.dedent(
        """
        import os
        import signal
        import time

        from frisk_sdk.process.install_frisk_shutdown import install_frisk_shutdown

        class Logger:
            def error(self, msg: str) -> None:
                print(f"LOG_ERROR:{msg}", flush=True)

        def shutdown_fn(timeout_s: float) -> None:
            print(f"SHUTDOWN:{timeout_s}", flush=True)

        # Ensure SIGTERM causes the process to *exit* after our handler runs.
        def prev_handler(sig, frame):
            raise SystemExit(128 + sig)

        signal.signal(signal.SIGTERM, prev_handler)

        install_frisk_shutdown(
            shutdown_fn,
            timeout_s=0.25,
            install_signals=True,
            logger=Logger(),
        )

        print("READY", flush=True)
        time.sleep(10)
        """
    )

    p = subprocess.Popen(
        [sys.executable, "-c", code],
        text=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )
    assert p.stdout is not None

    # Wait until the child has installed handlers.
    ready_deadline = time.time() + 3.0
    out = ""
    while "READY" not in out and time.time() < ready_deadline:
        line = p.stdout.readline()
        if not line:
            break
        out += line

    assert "READY" in out

    os.kill(p.pid, signal.SIGTERM)

    try:
        stdout, stderr = p.communicate(timeout=3.0)
    except subprocess.TimeoutExpired:
        p.kill()
        stdout, stderr = p.communicate(timeout=1.0)
        pytest.fail(
            f"Child did not exit after SIGTERM. stdout={stdout!r} stderr={stderr!r}"
        )

    combined = out + stdout
    assert "SHUTDOWN:" in combined


@pytest.mark.unit
def test_shutdown_handler_runs_on_normal_exit_via_atexit() -> None:
    code = textwrap.dedent(
        """
        from frisk_sdk.process.install_frisk_shutdown import install_frisk_shutdown

        class Logger:
            def error(self, msg: str) -> None:
                print(f"LOG_ERROR:{msg}", flush=True)

        def shutdown_fn(timeout_s: float) -> None:
            print("SHUTDOWN_ATEXIT", flush=True)

        install_frisk_shutdown(
            shutdown_fn,
            timeout_s=0.25,
            install_signals=False,
            logger=Logger(),
        )

        print("EXITING", flush=True)
        """
    )

    result = _run_in_subprocess(code)
    assert result.returncode == 0
    assert "EXITING" in result.stdout
    assert "SHUTDOWN_ATEXIT" in result.stdout


@pytest.mark.unit
def test_existing_sigterm_handler_is_chained_after_frisk_shutdown() -> None:
    code = textwrap.dedent(
        """
        import os
        import signal
        import time

        from frisk_sdk.process.install_frisk_shutdown import install_frisk_shutdown

        class Logger:
            def error(self, msg: str) -> None:
                print(f"LOG_ERROR:{msg}", flush=True)

        def prev_handler(sig, frame):
            print(f"PREV:{sig}", flush=True)
            raise SystemExit(128 + sig)

        signal.signal(signal.SIGTERM, prev_handler)

        def shutdown_fn(timeout_s: float) -> None:
            print(f"FRISK:{timeout_s}", flush=True)

        install_frisk_shutdown(
            shutdown_fn,
            timeout_s=0.25,
            install_signals=True,
            logger=Logger(),
        )

        print("READY", flush=True)
        time.sleep(10)
        """
    )

    p = subprocess.Popen(
        [sys.executable, "-c", code],
        text=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )
    assert p.stdout is not None

    ready_deadline = time.time() + 3.0
    out = ""
    while "READY" not in out and time.time() < ready_deadline:
        line = p.stdout.readline()
        if not line:
            break
        out += line

    assert "READY" in out

    os.kill(p.pid, signal.SIGTERM)

    stdout, stderr = p.communicate(timeout=3.0)
    combined = out + stdout

    frisk_idx = combined.find("FRISK:")
    prev_idx = combined.find("PREV:")

    assert frisk_idx != -1, (
        f"Expected FRISK shutdown output. stdout={combined!r} stderr={stderr!r}"
    )
    assert prev_idx != -1, (
        f"Expected PREV handler output. stdout={combined!r} stderr={stderr!r}"
    )
    assert frisk_idx < prev_idx, (
        "Expected frisk shutdown to run before the previous handler"
    )


@pytest.mark.unit
def test_sigkill_cannot_be_intercepted_by_shutdown_handler() -> None:
    """SIGKILL can't be caught/handled; verify we *don't* observe our handler running."""

    code = textwrap.dedent(
        """
        import time
        from frisk_sdk.process.install_frisk_shutdown import install_frisk_shutdown

        class Logger:
            def error(self, msg: str) -> None:
                print(f"LOG_ERROR:{msg}", flush=True)

        def shutdown_fn(timeout_s: float) -> None:
            print("SHUTDOWN_SIGKILL", flush=True)

        install_frisk_shutdown(
            shutdown_fn,
            timeout_s=0.25,
            install_signals=True,
            logger=Logger(),
        )

        print("READY", flush=True)
        time.sleep(10)
        """
    )

    p = subprocess.Popen(
        [sys.executable, "-c", code],
        text=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )
    assert p.stdout is not None

    ready_deadline = time.time() + 3.0
    out = ""
    while "READY" not in out and time.time() < ready_deadline:
        line = p.stdout.readline()
        if not line:
            break
        out += line

    assert "READY" in out

    os.kill(p.pid, signal.SIGKILL)

    stdout, stderr = p.communicate(timeout=3.0)
    combined = out + stdout

    assert "SHUTDOWN_SIGKILL" not in combined
    assert p.returncode not in (0, None)
