"""Tests for the local interactive self-update smoke harness."""

from __future__ import annotations

import subprocess
import sys
import zipfile
from importlib.machinery import SourceFileLoader
from pathlib import Path
from types import ModuleType

import pytest

ROOT = Path(__file__).resolve().parents[2]
SCRIPT = ROOT / "script" / "local-self-update-smoke"


def load_smoke_script() -> ModuleType:
    loader = SourceFileLoader("local_self_update_smoke", str(SCRIPT))
    module = ModuleType(loader.name)
    module.__file__ = str(SCRIPT)
    loader.exec_module(module)
    return module


def test_self_update_smoke_harness_prepares_fixture(tmp_path: Path) -> None:
    project = tmp_path / "self-update-smoke"

    result = subprocess.run(
        [
            sys.executable,
            str(SCRIPT),
            "--no-launch",
            "--project-dir",
            str(project),
            "--base-version",
            "2.2.0",
            "--next-version",
            "2.2.1-self-update-smoke",
        ],
        cwd=ROOT,
        check=True,
        text=True,
        capture_output=True,
    )

    assert "Self-update smoke fixture ready" in result.stdout
    assert "click Update" in result.stdout
    assert "a new Godot*.ips" in result.stdout

    base_cfg = (project / "addons" / "godot_ai" / "plugin.cfg").read_text(encoding="utf-8")
    assert 'version="2.2.0"' in base_cfg

    # The smoke patches land on the manager file; the dock keeps only
    # the visible banner UI.
    base_manager = (project / "addons" / "godot_ai" / "utils" / "update_manager.gd").read_text(
        encoding="utf-8"
    )
    assert 'const SELF_UPDATE_SMOKE_DOWNLOAD_URL := "smoke://local-prestaged"' in base_manager
    assert (
        'const SELF_UPDATE_SMOKE_ZIP := "res://self_update_smoke/godot-ai-plugin-vnext.zip"'
        in base_manager
    )
    assert "FileAccess.get_file_as_bytes(src)" in base_manager
    assert "user-update-path.txt" in base_manager

    base_configurator = (project / "addons" / "godot_ai" / "client_configurator.gd").read_text(
        encoding="utf-8"
    )
    assert "const DEFAULT_HTTP_PORT := 18000" in base_configurator
    assert "const DEFAULT_WS_PORT := 19500" in base_configurator
    assert 'const SELF_UPDATE_SMOKE_SERVER_VERSION := "2.2.0"' in base_configurator
    assert "var version := SELF_UPDATE_SMOKE_SERVER_VERSION" in base_configurator
    assert "return default_port" in base_configurator
    assert "static func ensure_settings_registered() -> void:" in base_configurator
    assert "static func _register_port_setting(" in base_configurator

    base_settings = (project / "addons" / "godot_ai" / "utils" / "settings.gd").read_text(
        encoding="utf-8"
    )
    assert "godot_ai_self_update_smoke/excluded_domains" in base_settings

    base_plugin = (project / "addons" / "godot_ai" / "plugin.gd").read_text(encoding="utf-8")
    assert "godot_ai_self_update_smoke/managed_server_pid" in base_plugin

    base_lifecycle = (project / "addons" / "godot_ai" / "utils" / "server_lifecycle.gd").read_text(
        encoding="utf-8"
    )
    assert 'const SELF_UPDATE_SMOKE_EXPECTED_SERVER_VERSION := "2.2.0"' in base_lifecycle
    assert "func _expected_server_version() -> String:" in base_lifecycle
    assert "return SELF_UPDATE_SMOKE_EXPECTED_SERVER_VERSION" in base_lifecycle

    zip_path = project / "self_update_smoke" / "godot-ai-plugin-vnext.zip"
    assert zip_path.exists()
    with zipfile.ZipFile(zip_path) as zf:
        names = set(zf.namelist())
        assert "addons/godot_ai/plugin.cfg" in names
        assert "addons/godot_ai/mcp_dock.gd" in names
        assert "addons/godot_ai/utils/update_manager.gd" in names
        assert "addons/godot_ai/utils/self_update_smoke_base.gd" in names
        assert "addons/godot_ai/utils/self_update_smoke_child.gd" in names
        vnext_cfg = zf.read("addons/godot_ai/plugin.cfg").decode()
        vnext_dock = zf.read("addons/godot_ai/mcp_dock.gd").decode()
        vnext_manager = zf.read("addons/godot_ai/utils/update_manager.gd").decode()
        vnext_configurator = zf.read("addons/godot_ai/client_configurator.gd").decode()
        vnext_settings = zf.read("addons/godot_ai/utils/settings.gd").decode()
        vnext_plugin = zf.read("addons/godot_ai/plugin.gd").decode()
        vnext_lifecycle = zf.read("addons/godot_ai/utils/server_lifecycle.gd").decode()
        vnext_base = zf.read("addons/godot_ai/utils/self_update_smoke_base.gd").decode()
        vnext_child = zf.read("addons/godot_ai/utils/self_update_smoke_child.gd").decode()

    assert 'version="2.2.1-self-update-smoke"' in vnext_cfg
    # The smoke download URL is no longer in the dock (it lives on the
    # manager); the dock should not contain it either.
    assert "smoke://local-prestaged" not in vnext_dock
    assert "smoke://local-prestaged" not in vnext_manager
    assert 'var _self_update_smoke_trigger: Dictionary = {"armed": true}' in vnext_dock
    assert 'var _self_update_smoke_array_trigger: Array[String] = ["armed"]' in vnext_dock
    assert "MCP | [self-update-smoke vnext _exit_tree]" in vnext_dock
    assert "SelfUpdateSmokeChild" in vnext_dock
    assert "class_name McpSelfUpdateSmokeBase" in vnext_base
    assert "class_name McpSelfUpdateSmokeChild" in vnext_child
    assert "extends McpSelfUpdateSmokeBase" in vnext_child
    assert "const DEFAULT_HTTP_PORT := 18000" in vnext_configurator
    assert 'const SELF_UPDATE_SMOKE_SERVER_VERSION := "2.2.0"' in vnext_configurator
    assert 'godot-ai==%s" % version' in vnext_configurator
    assert "var version := SELF_UPDATE_SMOKE_SERVER_VERSION" in vnext_configurator
    assert "return default_port" in vnext_configurator
    assert "static func ensure_settings_registered() -> void:" in vnext_configurator
    assert "static func _register_port_setting(" in vnext_configurator
    assert "godot_ai_self_update_smoke/excluded_domains" in vnext_settings
    assert "godot_ai_self_update_smoke/managed_server_pid" in vnext_plugin
    assert 'const SELF_UPDATE_SMOKE_EXPECTED_SERVER_VERSION := "2.2.0"' in vnext_lifecycle
    assert "func _expected_server_version() -> String:" in vnext_lifecycle
    assert "return SELF_UPDATE_SMOKE_EXPECTED_SERVER_VERSION" in vnext_lifecycle


def test_self_update_smoke_log_verifier_rejects_external_adoption() -> None:
    smoke = load_smoke_script()
    lines = [
        "MCP | foreign server already running on port 18000, using existing",
        "MCP | self-update smoke: staged local zip /tmp/update.zip",
        "MCP | stopped server (PID [123])",
        "MCP | update runner enabling new plugin",
    ]

    assert smoke.smoke_adopted_existing_server_before_update(lines)
    assert not smoke.smoke_started_own_server_before_update(lines)
    assert smoke.smoke_stopped_server_during_update(lines)


def test_self_update_smoke_log_verifier_requires_managed_stop_after_staging() -> None:
    smoke = load_smoke_script()
    lines = [
        "MCP | started server (PID 123, v2.2.1): godot-ai",
        "MCP | self-update smoke: staged local zip /tmp/update.zip",
        "MCP | update runner enabling new plugin",
    ]

    assert smoke.smoke_started_own_server_before_update(lines)
    assert not smoke.smoke_adopted_existing_server_before_update(lines)
    assert not smoke.smoke_stopped_server_during_update(lines)


def test_self_update_smoke_log_verifier_rejects_version_mismatch() -> None:
    smoke = load_smoke_script()
    lines = [
        "MCP | started server (PID 123, v2.2.0): godot-ai",
        "MCP | self-update smoke: staged local zip /tmp/update.zip",
        "MCP | stopped server (PID [123])",
        "MCP | update runner enabling new plugin",
        "MCP | plugin loaded",
        (
            "MCP | Port 18000 is occupied by godot-ai server v2.2.0; "
            "plugin expects v2.2.1. Stop the old server or change both HTTP and WS ports."
        ),
    ]

    assert smoke.smoke_reported_server_version_mismatch(lines)


def test_self_update_smoke_log_verifier_accepts_matching_versions() -> None:
    smoke = load_smoke_script()
    lines = [
        "MCP | started server (PID 123, v2.2.0): godot-ai",
        "MCP | self-update smoke: staged local zip /tmp/update.zip",
        "MCP | stopped server (PID [123])",
        "MCP | update runner enabling new plugin",
        "MCP | started server (PID 456, v2.2.0): godot-ai",
        "MCP | plugin loaded",
    ]

    assert not smoke.smoke_reported_server_version_mismatch(lines)


def test_self_update_smoke_harness_refuses_unmarked_existing_dir(tmp_path: Path) -> None:
    project = tmp_path / "existing-project"
    project.mkdir()
    (project / "project.godot").write_text("not generated by the harness\n")

    result = subprocess.run(
        [
            sys.executable,
            str(SCRIPT),
            "--no-launch",
            "--project-dir",
            str(project),
        ],
        cwd=ROOT,
        text=True,
        capture_output=True,
    )

    assert result.returncode != 0
    assert "not marked as a smoke fixture" in result.stderr


def _make_addon_with(tmp_path: Path, *, present: tuple[str, ...]) -> Path:
    addon = tmp_path / "addon"
    (addon / "utils").mkdir(parents=True)
    for rel in present:
        (addon / rel).write_text("# fixture stub\n", encoding="utf-8")
    return addon


def test_v240_preflight_passes_when_both_files_present(tmp_path: Path) -> None:
    smoke = load_smoke_script()
    addon = _make_addon_with(
        tmp_path,
        present=("utils/server_lifecycle.gd", "utils/update_manager.gd"),
    )
    # Validator returns None on success and raises HarnessError otherwise; the
    # assertion both documents intent and trips the runner's zero-assertion guard.
    assert smoke._require_v240_plus_addon_shape(addon, "2.4.0") is None


@pytest.mark.parametrize(
    ("present", "expected_missing"),
    [
        (("utils/update_manager.gd",), "utils/server_lifecycle.gd"),
        (("utils/server_lifecycle.gd",), "utils/update_manager.gd"),
    ],
)
def test_v240_preflight_raises_clear_harness_error_for_single_missing_file(
    tmp_path: Path, present: tuple[str, ...], expected_missing: str
) -> None:
    smoke = load_smoke_script()
    addon = _make_addon_with(tmp_path, present=present)
    with pytest.raises(smoke.HarnessError) as exc_info:
        smoke._require_v240_plus_addon_shape(addon, "2.3.2")
    message = str(exc_info.value)
    assert expected_missing in message, message
    assert "2.3.2" in message, message
    assert "v2.4.0" in message, message
    assert "--base-from-release-tag" in message, message


def test_v240_preflight_lists_all_missing_files_when_both_absent(tmp_path: Path) -> None:
    smoke = load_smoke_script()
    addon = _make_addon_with(tmp_path, present=())
    with pytest.raises(smoke.HarnessError) as exc_info:
        smoke._require_v240_plus_addon_shape(addon, "2.3.2")
    message = str(exc_info.value)
    assert "utils/server_lifecycle.gd" in message, message
    assert "utils/update_manager.gd" in message, message


def test_self_update_smoke_harness_refuses_suspicious_marker(tmp_path: Path) -> None:
    project = tmp_path / "existing-project"
    (project / ".godot-ai-self-update-smoke").mkdir(parents=True)
    (project / ".godot-ai-self-update-smoke" / "marker.txt").write_text("marker\n")
    (project / "project.godot").write_text("not generated by the harness\n")

    result = subprocess.run(
        [
            sys.executable,
            str(SCRIPT),
            "--no-launch",
            "--project-dir",
            str(project),
        ],
        cwd=ROOT,
        text=True,
        capture_output=True,
    )

    assert result.returncode != 0
    assert "has a smoke marker but does not look generated" in result.stderr
