"""dashboard-test-runner US-001: opt-in, admin-only tracker test-runner API.

Runs against a tiny throwaway repo (tmp_path) so collection and runs stay
fast and hermetic — the manager itself discovers the real repo root when
constructed without an explicit ``repo_root``.
"""

import json
import sys
import time
import urllib.error
import urllib.request
from pathlib import Path

import pytest

from meshnet_tracker.accounts import AccountStore
from meshnet_tracker.server import TrackerServer
from meshnet_tracker.test_runner import TestRunManager as RunManager
from meshnet_tracker.test_runner import _test_python, discover_repo_root


def _make_repo(tmp_path: Path) -> Path:
    """A minimal repo satisfying discover_repo_root's markers, with fast tests."""
    (tmp_path / "pyproject.toml").write_text("[tool.pytest.ini_options]\n")
    tests = tmp_path / "tests"
    tests.mkdir()
    (tests / "conftest.py").write_text("")
    (tests / "test_quick.py").write_text(
        "def test_passes():\n    assert True\n\n"
        "def test_also_passes():\n    assert 1 + 1 == 2\n"
    )
    (tests / "test_slow.py").write_text(
        "import time\n\ndef test_sleeps():\n    time.sleep(30)\n"
    )
    (tests / "test_smoke.py").write_text("def test_smoke():\n    assert True\n")
    (tests / "test_cache.py").write_text(
        'def test_cache_roundtrip():\n    """Cache roundtrip remains deterministic."""\n    assert True\n'
    )
    # Real-inference stand-in: must never be collected or run by default.
    (tests / "test_real_stub.py").write_text(
        "def test_real_inference():\n    raise AssertionError('env-gated')\n"
    )
    return tmp_path


def _start_tracker(tmp_path: Path | None):
    """Tracker + (admin_session, user_session); test runner enabled iff tmp_path given."""
    accounts = AccountStore()
    admin = accounts.register(email="admin@example.com", password="admin-pass-123")
    admin_session = accounts.create_session(admin["account_id"])
    user = accounts.register(email="user@example.com", password="user-pass-123")
    user_session = accounts.create_session(user["account_id"])
    runner = RunManager(repo_root=_make_repo(tmp_path)) if tmp_path is not None else None
    tracker = TrackerServer(accounts=accounts, test_runner=runner)
    port = tracker.start()
    return tracker, port, admin_session, user_session


def _request(port, method, path, token=None, payload=None):
    body = json.dumps(payload).encode() if payload is not None else None
    headers = {"Content-Type": "application/json"}
    if token:
        headers["Authorization"] = f"Bearer {token}"
    req = urllib.request.Request(
        f"http://127.0.0.1:{port}{path}", data=body, headers=headers, method=method
    )
    try:
        with urllib.request.urlopen(req) as resp:
            return resp.status, json.loads(resp.read())
    except urllib.error.HTTPError as exc:
        return exc.code, json.loads(exc.read())


def _wait_for_completion(port, session, timeout=90.0):
    deadline = time.time() + timeout
    while time.time() < deadline:
        status, data = _request(port, "GET", "/v1/tests/status", token=session)
        assert status == 200
        if data["run"] is not None and data["run"]["status"] != "running":
            return data
        time.sleep(0.2)
    raise AssertionError("test run did not finish in time")


def test_endpoints_require_admin(tmp_path):
    "Endpoints require admin\n\nTags: dashboard, routing, test-runner, tracker"
    tracker, port, _admin, user = _start_tracker(tmp_path)
    try:
        assert _request(port, "GET", "/v1/tests")[0] == 401
        assert _request(port, "GET", "/v1/tests/status")[0] == 401
        assert _request(port, "POST", "/v1/tests/run", payload={"target": "x"})[0] == 401
        assert _request(port, "GET", "/v1/tests", token=user)[0] == 403
        assert _request(port, "GET", "/v1/tests/status", token=user)[0] == 403
        assert _request(
            port, "POST", "/v1/tests/run", token=user, payload={"target": "x"}
        )[0] == 403
    finally:
        tracker.stop()


def test_disabled_by_default_even_for_admin(monkeypatch):
    "Disabled by default even for admin\n\nTags: dashboard, routing, test-runner, tracker"
    monkeypatch.delenv("MESHNET_ENABLE_TEST_RUNNER", raising=False)
    tracker, port, admin, _user = _start_tracker(None)
    try:
        assert tracker._test_runner is None  # no flag, no env → fails closed
        for method, path, payload in [
            ("GET", "/v1/tests", None),
            ("GET", "/v1/tests/status", None),
            ("POST", "/v1/tests/run", {"target": "tests/test_smoke.py::test_smoke"}),
        ]:
            status, data = _request(port, method, path, token=admin, payload=payload)
            assert status == 403
            assert "disabled" in data["error"]
    finally:
        tracker.stop()


def test_enable_flag_constructs_runner_against_real_repo(monkeypatch):
    "Enable flag constructs runner against real repo\n\nTags: dashboard, routing, test-runner, tracker"
    monkeypatch.delenv("MESHNET_ENABLE_TEST_RUNNER", raising=False)
    tracker = TrackerServer(enable_test_runner=True)
    assert tracker._test_runner is not None
    assert tracker._test_runner.repo_root == discover_repo_root()
    assert (tracker._test_runner.repo_root / "tests" / "conftest.py").is_file()


def test_runner_uses_configured_project_python(monkeypatch, tmp_path):
    "Runner uses configured project python\n\nTags: dashboard, routing, test-runner, tracker"
    python = tmp_path / "python"
    python.touch(mode=0o755)
    monkeypatch.setenv("MESHNET_PYTHON", str(python))
    assert _test_python() == str(python)

    monkeypatch.setenv("MESHNET_PYTHON", str(tmp_path / "missing-python"))
    assert _test_python() == sys.executable


def test_collection_lists_tests_and_excludes_real_inference(tmp_path, monkeypatch):
    "Collection lists tests and excludes real inference\n\nTags: dashboard, routing, test-runner, tracker"
    monkeypatch.delenv("MESHNET_ENABLE_REAL_INFERENCE_TESTS", raising=False)
    tracker, port, admin, _user = _start_tracker(tmp_path)
    try:
        status, data = _request(port, "GET", "/v1/tests", token=admin)
        assert status == 200
        assert data["enabled"] is True
        assert "tests/test_quick.py::test_passes" in data["tests"]
        assert "tests/test_slow.py::test_sleeps" in data["tests"]
        metadata = {item["id"]: item for item in data["test_metadata"]}
        assert metadata["tests/test_cache.py::test_cache_roundtrip"]["description"] == (
            "Cache roundtrip remains deterministic."
        )
        assert "cache" in metadata["tests/test_cache.py::test_cache_roundtrip"]["tags"]
        assert any(tag["id"] == "tag:cache" for tag in data["tags"])
        assert not any("test_real_" in node_id for node_id in data["tests"])
        assert data["collected_at"] is not None
        suite_ids = [suite["id"] for suite in data["suites"]]
        assert "suite:smoke" in suite_ids
        assert "suite:real-inference" not in suite_ids
    finally:
        tracker.stop()


def test_run_selected_test_to_completion(tmp_path):
    "Run selected test to completion\n\nTags: dashboard, routing, test-runner, tracker"
    tracker, port, admin, _user = _start_tracker(tmp_path)
    try:
        assert _request(port, "GET", "/v1/tests", token=admin)[0] == 200
        status, started = _request(
            port, "POST", "/v1/tests/run", token=admin,
            payload={"target": "tests/test_quick.py::test_passes"},
        )
        assert status == 202
        assert started["run"]["status"] == "running"
        assert started["run"]["run_id"]
        assert started["run"]["started_at"] is not None

        finished = _wait_for_completion(port, admin)
        run = finished["run"]
        assert run["status"] == "passed"
        assert run["exit_code"] == 0
        assert run["target"] == "tests/test_quick.py::test_passes"
        assert run["finished_at"] >= run["started_at"]
        assert run["elapsed_seconds"] >= 0
        assert "1 passed" in finished["stdout"]
        assert finished["log_lines_limit"] > 0
    finally:
        tracker.stop()


def test_run_approved_suite_without_prior_collection(tmp_path):
    "Run approved suite without prior collection\n\nTags: dashboard, routing, test-runner, tracker"
    tracker, port, admin, _user = _start_tracker(tmp_path)
    try:
        status, started = _request(
            port, "POST", "/v1/tests/run", token=admin,
            payload={"target": "suite:smoke"},
        )
        assert status == 202
        assert started["run"]["args"] == ["tests/test_smoke.py"]
        finished = _wait_for_completion(port, admin)
        assert finished["run"]["status"] == "passed"
        assert finished["run"]["exit_code"] == 0
    finally:
        tracker.stop()


def test_run_all_and_tag_targets(tmp_path):
    "Run all and tag targets\n\nTags: dashboard, routing, test-runner, tracker"
    tracker, port, admin, _user = _start_tracker(tmp_path)
    try:
        assert _request(port, "GET", "/v1/tests", token=admin)[0] == 200
        runner = tracker._test_runner
        assert runner is not None
        all_state = runner.start("suite:all")
        assert all_state["run"]["target"] == "suite:all"
        assert len(all_state["run"]["args"]) == len(runner._collected)
    finally:
        tracker.stop()

    second_repo = tmp_path / "second"
    second_repo.mkdir()
    tracker, _port, _admin, _user = _start_tracker(second_repo)
    try:
        runner = tracker._test_runner
        assert runner is not None
        runner.collect()
        tagged = runner.start("tag:cache")
        assert tagged["run"]["args"] == ["tests/test_cache.py::test_cache_roundtrip"]
    finally:
        tracker.stop()


@pytest.mark.parametrize("target", [
    "",
    "-x",
    "--help",
    "tests/test_quick.py::test_passes; rm -rf /",
    "tests/never_collected.py::test_missing",
    "suite:real-inference",
    "suite:no-such-suite",
])
def test_rejects_arbitrary_or_uncollected_targets(tmp_path, target, monkeypatch):
    "Rejects arbitrary or uncollected targets\n\nTags: dashboard, routing, test-runner, tracker"
    monkeypatch.delenv("MESHNET_ENABLE_REAL_INFERENCE_TESTS", raising=False)
    tracker, port, admin, _user = _start_tracker(tmp_path)
    try:
        assert _request(port, "GET", "/v1/tests", token=admin)[0] == 200
        status, data = _request(
            port, "POST", "/v1/tests/run", token=admin, payload={"target": target}
        )
        assert status == 400
        assert "error" in data
        # Nothing started.
        assert _request(port, "GET", "/v1/tests/status", token=admin)[1]["run"] is None
    finally:
        tracker.stop()


def test_rejects_concurrent_runs(tmp_path):
    "Rejects concurrent runs\n\nTags: dashboard, routing, test-runner, tracker"
    tracker, port, admin, _user = _start_tracker(tmp_path)
    try:
        assert _request(port, "GET", "/v1/tests", token=admin)[0] == 200
        status, _ = _request(
            port, "POST", "/v1/tests/run", token=admin,
            payload={"target": "tests/test_slow.py::test_sleeps"},
        )
        assert status == 202
        status, data = _request(
            port, "POST", "/v1/tests/run", token=admin,
            payload={"target": "tests/test_quick.py::test_passes"},
        )
        assert status == 409
        assert "in progress" in data["error"]
    finally:
        # stop() kills the sleeping pytest child via TestRunManager.shutdown().
        tracker.stop()
