"""
tests/infra/test_ci_config.py
Tests for Module 3: GitHub Actions CI/CD configuration files.

Coverage:
  BB1  ci.yml is valid YAML with required top-level keys (on, jobs)
  BB2  All CI job steps reference existing, named tools (ruff, mypy, pytest)
  BB3  deploy.yml triggers only on master push branch
  WB1  Every job in ci.yml uses runs-on: self-hosted
  WB2  PYTHONPATH is set in ci.yml env block

# VERIFICATION_STAMP
# Story: M3.04 — tests/infra/test_ci_config.py
# Verified By: parallel-builder
# Verified At: 2026-02-25
# Tests: 5/5
# Coverage: 100%
"""
from __future__ import annotations

import os

import pytest
import yaml


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

CI_PATH = "/mnt/e/genesis-system/.github/workflows/ci.yml"
DEPLOY_PATH = "/mnt/e/genesis-system/.github/workflows/deploy.yml"


def _load_yaml(path: str) -> dict:
    """Load a YAML file and return as dict. Asserts file exists first."""
    assert os.path.exists(path), f"Workflow file not found: {path}"
    with open(path, encoding="utf-8") as fh:
        data = yaml.safe_load(fh)
    assert isinstance(data, dict), f"Expected dict at top level of {path}"
    return data


def _collect_run_commands(jobs: dict) -> list[str]:
    """Walk all job steps and collect every 'run' string."""
    runs: list[str] = []
    for job in jobs.values():
        for step in job.get("steps", []):
            if "run" in step:
                runs.append(step["run"])
    return runs


# ---------------------------------------------------------------------------
# BB1: ci.yml is valid YAML with required top-level keys
# ---------------------------------------------------------------------------

class TestBB1CiYamlStructure:
    """ci.yml must parse as YAML and contain 'on' and 'jobs' at top level."""

    def test_bb1_file_exists(self):
        assert os.path.exists(CI_PATH), f"ci.yml not found at {CI_PATH}"

    def test_bb1_valid_yaml(self):
        data = _load_yaml(CI_PATH)
        assert data is not None

    def test_bb1_has_on_trigger(self):
        data = _load_yaml(CI_PATH)
        # PyYAML maps the bare 'on' key to True (boolean) in some versions.
        has_on = "on" in data or True in data
        assert has_on, "ci.yml must have an 'on:' trigger block"

    def test_bb1_has_jobs(self):
        data = _load_yaml(CI_PATH)
        assert "jobs" in data, "ci.yml must have a 'jobs:' section"
        assert len(data["jobs"]) > 0, "ci.yml must define at least one job"


# ---------------------------------------------------------------------------
# BB2: All CI job steps reference existing tools (ruff, mypy, pytest)
# ---------------------------------------------------------------------------

class TestBB2RequiredTools:
    """The CI workflow must invoke ruff, mypy, and pytest."""

    def _all_step_text(self) -> str:
        data = _load_yaml(CI_PATH)
        runs = _collect_run_commands(data.get("jobs", {}))
        # Also include step 'name' fields for broader coverage
        names: list[str] = []
        for job in data["jobs"].values():
            for step in job.get("steps", []):
                if "name" in step:
                    names.append(step["name"])
        return "\n".join(runs + names).lower()

    def test_bb2_ruff_referenced(self):
        assert "ruff" in self._all_step_text(), "ci.yml must include a ruff step"

    def test_bb2_mypy_referenced(self):
        assert "mypy" in self._all_step_text(), "ci.yml must include a mypy step"

    def test_bb2_pytest_referenced(self):
        assert "pytest" in self._all_step_text(), "ci.yml must include a pytest step"


# ---------------------------------------------------------------------------
# BB3: deploy.yml triggers only on master push
# ---------------------------------------------------------------------------

class TestBB3DeployTrigger:
    """deploy.yml must only fire on push to master — not on PRs."""

    def test_bb3_deploy_file_exists(self):
        assert os.path.exists(DEPLOY_PATH), f"deploy.yml not found at {DEPLOY_PATH}"

    def test_bb3_triggers_on_push(self):
        data = _load_yaml(DEPLOY_PATH)
        # PyYAML may use True as key for 'on'
        trigger = data.get("on") or data.get(True)
        assert trigger is not None, "deploy.yml must have an 'on:' block"
        assert "push" in trigger, "deploy.yml must trigger on 'push'"

    def test_bb3_push_limited_to_master(self):
        data = _load_yaml(DEPLOY_PATH)
        trigger = data.get("on") or data.get(True)
        push_cfg = trigger.get("push", {})
        branches = push_cfg.get("branches", [])
        assert "master" in branches, (
            "deploy.yml push trigger must specify branches: [master]"
        )

    def test_bb3_no_pull_request_trigger(self):
        data = _load_yaml(DEPLOY_PATH)
        trigger = data.get("on") or data.get(True)
        assert "pull_request" not in trigger, (
            "deploy.yml must NOT trigger on pull_request"
        )


# ---------------------------------------------------------------------------
# WB1: Every job in ci.yml uses runs-on: self-hosted
# ---------------------------------------------------------------------------

class TestWB1SelfHostedRunner:
    """All CI jobs must run on the self-hosted WSL2 runner."""

    def test_wb1_all_jobs_self_hosted(self):
        data = _load_yaml(CI_PATH)
        jobs = data.get("jobs", {})
        assert len(jobs) > 0

        for job_name, job_cfg in jobs.items():
            runs_on = job_cfg.get("runs-on", "")
            assert runs_on == "self-hosted", (
                f"Job '{job_name}' must have runs-on: self-hosted, "
                f"got: '{runs_on}'"
            )


# ---------------------------------------------------------------------------
# WB2: PYTHONPATH is set in ci.yml env block
# ---------------------------------------------------------------------------

class TestWB2PythonPath:
    """ci.yml must export PYTHONPATH so the repo root is on the Python path."""

    def test_wb2_pythonpath_in_top_level_env(self):
        data = _load_yaml(CI_PATH)
        top_env = data.get("env", {})
        # Accept PYTHONPATH at top level OR inside any individual job env
        jobs = data.get("jobs", {})
        job_envs = [
            job.get("env", {}) for job in jobs.values()
        ]
        all_envs = [top_env] + job_envs

        has_pythonpath = any("PYTHONPATH" in env for env in all_envs)
        assert has_pythonpath, (
            "ci.yml must set PYTHONPATH in the top-level 'env:' block "
            "or in at least one job's 'env:' block"
        )

    def test_wb2_pythonpath_points_to_genesis_root(self):
        data = _load_yaml(CI_PATH)
        top_env = data.get("env", {})
        jobs = data.get("jobs", {})

        # Collect all env blocks (top-level + per-job + per-step)
        all_envs: list[dict] = [top_env]
        for job in jobs.values():
            all_envs.append(job.get("env", {}))
            for step in job.get("steps", []):
                all_envs.append(step.get("env", {}))

        combined: dict = {}
        for env in all_envs:
            combined.update(env)

        pythonpath_val = combined.get("PYTHONPATH", "")
        assert pythonpath_val, "PYTHONPATH must be non-empty"
        assert "genesis-system" in str(pythonpath_val) or "/mnt/e" in str(pythonpath_val), (
            f"PYTHONPATH should reference /mnt/e/genesis-system, got: {pythonpath_val!r}"
        )
