"""Tests for ``.opencode/scripts/project_contract.py`` — single source of
truth for project types, stacks, structure, frontend markers.

The contract module is imported via ``importlib.util`` (no ``sys.path``
mutation), mirroring how ``spec-status.py`` and ``project-status.py`` load it.
"""

from __future__ import annotations

import importlib.util
import sys
from pathlib import Path

SCRIPT_PATH = (
    Path(__file__).resolve().parent.parent / ".opencode" / "scripts" / "project_contract.py"
)
_spec = importlib.util.spec_from_file_location("project_contract", SCRIPT_PATH)
pc = importlib.util.module_from_spec(_spec)  # type: ignore[arg-type]
_spec.loader.exec_module(pc)  # type: ignore[union-attr]


# ── ProjectType enum ────────────────────────────────────────────────────────


def test_project_type_enum_values():
    """ProjectType has exactly 7 members including MCP_SERVER and UNKNOWN."""
    members = {t.name for t in pc.ProjectType}
    assert members == {
        "FULLSTACK",
        "BACKEND",
        "CLI",
        "BOT",
        "WORKER",
        "MCP_SERVER",
        "UNKNOWN",
    }, f"unexpected members: {members}"
    # Values are lowercase strings.
    values = {t.value for t in pc.ProjectType}
    assert values == {
        "fullstack",
        "backend",
        "cli",
        "bot",
        "worker",
        "mcp-server",
        "unknown",
    }


def test_valid_types_excludes_unknown():
    """VALID_TYPES excludes ``"unknown"`` (used by spec-status PROJECT_TYPE)."""
    assert "unknown" not in pc.VALID_TYPES
    assert "mcp-server" in pc.VALID_TYPES
    assert {
        "fullstack",
        "backend",
        "cli",
        "bot",
        "worker",
        "mcp-server",
    } == pc.VALID_TYPES


def test_stack_required_keys_match_valid_types():
    """STACK_REQUIRED keys == VALID_TYPES (all 6 types including mcp-server)."""
    assert set(pc.STACK_REQUIRED.keys()) == pc.VALID_TYPES


def test_stack_required_uv_in_all_types():
    """``uv`` stays in ALL stacks (build-tool, mentioned in stack.md)."""
    for ptype, required in pc.STACK_REQUIRED.items():
        assert "uv" in required, f"missing 'uv' in {ptype} stack: {required}"


def test_structure_expected_keys_subset():
    """STRUCTURE_EXPECTED keys ⊆ VALID_TYPES ∪ {"unknown"}.

    BACKEND is resolved dynamically (not in dict); mcp-server TBD out of scope.
    """
    keys = set(pc.STRUCTURE_EXPECTED.keys())
    valid_plus_unknown = pc.VALID_TYPES | {"unknown"}
    assert keys.issubset(valid_plus_unknown), f"unexpected keys: {keys - valid_plus_unknown}"
    # Backend NOT in dict (resolved dynamically via _expected_backend_paths)
    assert "backend" not in keys
    # mcp-server NOT in dict (out of scope)
    assert "mcp-server" not in keys


def test_frontend_stack_markers_keys():
    """FRONTEND_STACK_MARKERS has the expected keys with correct marker lists."""
    assert set(pc.FRONTEND_STACK_MARKERS.keys()) == {
        "fullstack_package_deps",
        "fullstack_files",
    }
    assert pc.FRONTEND_STACK_MARKERS["fullstack_package_deps"] == [
        "tailwindcss",
        "bits-ui",
    ]
    assert pc.FRONTEND_STACK_MARKERS["fullstack_files"] == [
        "frontend/components.json",
        "frontend/tsconfig.json",
    ]


def test_mobile_first_markers_keys():
    """MOBILE_FIRST_MARKERS (issue #278) has the expected keys.

    Distinct from FRONTEND_STACK_MARKERS so the stack check stays focused on
    the Tailwind/shadcn/TS trio while mobile-first (PWA + mobile Playwright +
    a11y) is a separate dict.
    """
    assert set(pc.MOBILE_FIRST_MARKERS.keys()) == {
        "fullstack_files",
        "fullstack_app_html_markers",
    }
    assert pc.MOBILE_FIRST_MARKERS["fullstack_files"] == [
        "frontend/static/manifest.webmanifest",
        "frontend/tests/e2e/mobile.spec.ts",
        "frontend/tests/e2e/accessibility.spec.ts",
    ]
    assert pc.MOBILE_FIRST_MARKERS["fullstack_app_html_markers"] == [
        "viewport",
        "manifest",
    ]


# ── re-export sanity (backward compat) ─────────────────────────────────────


def test_spec_status_reexports_match_contract():
    """``spec-status.py`` re-exports VALID_TYPES / STACK_REQUIRED from contract."""
    spec_status_path = (
        Path(__file__).resolve().parent.parent / ".opencode" / "scripts" / "spec-status.py"
    )
    ss_spec = importlib.util.spec_from_file_location("spec_status", spec_status_path)
    ss = importlib.util.module_from_spec(ss_spec)  # type: ignore[arg-type]
    sys.modules["spec_status"] = ss
    ss_spec.loader.exec_module(ss)  # type: ignore[union-attr]
    assert ss.VALID_TYPES is pc.VALID_TYPES or ss.VALID_TYPES == pc.VALID_TYPES
    assert ss.STACK_REQUIRED == pc.STACK_REQUIRED


def test_project_status_reexports_match_contract():
    """``project-status.py`` re-exports ProjectType / STRUCTURE_EXPECTED."""
    project_status_path = (
        Path(__file__).resolve().parent.parent / ".opencode" / "scripts" / "project-status.py"
    )
    ps_spec = importlib.util.spec_from_file_location("project_status", project_status_path)
    ps = importlib.util.module_from_spec(ps_spec)  # type: ignore[arg-type]
    sys.modules["project_status"] = ps
    ps_spec.loader.exec_module(ps)  # type: ignore[union-attr]
    # Note: ProjectType is loaded twice (once here, once inside project-status
    # via importlib.util with a different module name) so identity differs,
    # but the enum members compare equal by value.
    assert list(ps.ProjectType) == list(pc.ProjectType)
    assert ps.STRUCTURE_EXPECTED == pc.STRUCTURE_EXPECTED
