"""Tests for orchestration modules: git_ops, status, prompt, config, iteration."""

import json
import os
import subprocess
from pathlib import Path

import pytest

import common
import config as config_mod
import git_ops
import iteration
import prompt
import status
from conftest import (
    PROJECT_ID,
    SAMPLE_EXECUTED,
)


# ═══════════════════════════════════════════════════════════════════════════
# git_ops
# ═══════════════════════════════════════════════════════════════════════════


class TestGetCurrentBranch:
    def test_returns_branch_in_git_repo(self, tmp_path: Path):
        subprocess.run(
            ["git", "init"], cwd=str(tmp_path), capture_output=True
        )
        subprocess.run(
            ["git", "commit", "--allow-empty", "-m", "init"],
            cwd=str(tmp_path),
            capture_output=True,
            env={
                **os.environ,
                "GIT_AUTHOR_NAME": "test",
                "GIT_COMMITTER_NAME": "test",
                "GIT_AUTHOR_EMAIL": "t@t",
                "GIT_COMMITTER_EMAIL": "t@t",
            },
        )
        branch = git_ops.get_current_branch(tmp_path)
        assert branch in ("main", "master")

    def test_non_git_dir_returns_main(self, tmp_path: Path):
        assert git_ops.get_current_branch(tmp_path) == "main"


# ═══════════════════════════════════════════════════════════════════════════
# status
# ═══════════════════════════════════════════════════════════════════════════


class TestWriteStatus:
    def test_writes_file(self, project_dir: Path):
        status_path = project_dir / ".autobuilder" / "status.md"
        status.write_status(
            status_path, PROJECT_ID, 5, "building", "Working...",
            project_dir=project_dir,
        )
        content = status_path.read_text()
        assert PROJECT_ID in content
        assert "building" in content
        assert "5" in content


class TestPidFile:
    def test_write_and_remove(self, project_dir: Path):
        status.write_pid_file(project_dir)
        pid_file = project_dir / ".autobuilder" / "daemon.pid"
        assert pid_file.exists()
        assert str(os.getpid()) in pid_file.read_text()
        status.remove_pid_file(project_dir)
        assert not pid_file.exists()


class TestCheckStopSignal:
    def test_returns_false_when_absent(self, project_dir: Path):
        assert status.check_stop_signal(project_dir) is False

    def test_returns_true_and_deletes(self, project_dir: Path):
        stop_file = project_dir / ".autobuilder" / "stop"
        stop_file.write_text("stop")
        assert status.check_stop_signal(project_dir) is True
        assert not stop_file.exists()


# ═══════════════════════════════════════════════════════════════════════════
# prompt — review strategy & prompt builder
# ═══════════════════════════════════════════════════════════════════════════


class TestPickReviewStrategy:
    def test_rotates_through_all(self):
        seen = set()
        for i in range(1, len(common.REVIEW_STRATEGIES) + 1):
            s = prompt.pick_review_strategy(i)
            seen.add(s["name"])
        assert len(seen) == len(common.REVIEW_STRATEGIES)

    def test_wraps_around(self):
        n = len(common.REVIEW_STRATEGIES)
        assert (
            prompt.pick_review_strategy(1)["name"]
            == prompt.pick_review_strategy(n + 1)["name"]
        )

    def test_deterministic(self):
        assert (
            prompt.pick_review_strategy(3)["name"]
            == prompt.pick_review_strategy(3)["name"]
        )


class TestBuildPhasePrompt:
    """Cover the first-phase prompt — full header + reference material + STEP."""

    def _build(self, tasks_dir: Path, *, phase: str = "review", **overrides):
        project_root = tasks_dir.parent.parent.parent.parent
        defaults = dict(
            phase=phase,
            include_header=True,
            project_dir=project_root,
            project_id=PROJECT_ID,
            root_initiative=PROJECT_ID,
            tasks_dir=tasks_dir,
            tasks_rel=".autobuilder/initiatives/0001-test-app/tasks/",
            active_initiative="0001-test-app",
            initiative_spec="",
            initiative_constraints="",
            iteration=1,
            backlog="",
            phases=["review", "plan", "execute"],
            review_mode="polish",
            review_strategy=common.REVIEW_STRATEGIES[0],
            env_vars=[],
            project_context="",
            initiative_map="",
            global_executed_summary="",
        )
        defaults.update(overrides)
        return prompt.build_phase_prompt(**defaults)

    def test_contains_project_info(self, tasks_dir: Path):
        p = self._build(tasks_dir)
        assert PROJECT_ID in p
        assert "iteration #1" in p
        assert "ACTIVE INITIATIVE: 0001-test-app" in p

    def test_includes_project_context(self, tasks_dir: Path):
        p = self._build(tasks_dir, project_context="Trading platform for retail investors.")
        assert "Trading platform for retail investors." in p
        assert "PROJECT CONTEXT" in p

    def test_skips_project_context_when_empty(self, tasks_dir: Path):
        p = self._build(tasks_dir, project_context="")
        assert "PROJECT CONTEXT" not in p

    def test_includes_initiative_map(self, tasks_dir: Path):
        p = self._build(tasks_dir, initiative_map="0001-app [active — 3/12 executed]")
        assert "0001-app [active" in p
        assert "INITIATIVES" in p

    def test_includes_backlog(self, tasks_dir: Path):
        p = self._build(tasks_dir, backlog="Fix the login page")
        assert "Fix the login page" in p
        assert "product owner" in p

    def test_includes_review_strategy(self, tasks_dir: Path):
        strategy = common.REVIEW_STRATEGIES[1]
        p = self._build(tasks_dir, review_strategy=strategy)
        assert strategy["name"] in p

    def test_execute_phase_omits_review_step(self, tasks_dir: Path):
        p = self._build(tasks_dir, phase="execute")
        assert "PRODUCT REVIEW" not in p
        assert "EXECUTE" in p

    def test_plan_per_task_subset_focuses_on_one_task(self, tasks_dir: Path):
        md = tasks_dir / "0010-foo.md"
        md.write_text("---\nid: foo\n---\n")
        p = self._build(
            tasks_dir, phase="plan",
            task_subset=[("foo", md)],
            task_progress=(2, 4),
        )
        assert "EXACTLY this one task" in p
        assert "0010-foo.md" in p
        assert "foo" in p
        assert "call 2 of 4" in p

    def test_execute_per_task_subset_says_one_commit(self, tasks_dir: Path):
        md = tasks_dir / "0010-bar.md"
        md.write_text("---\nid: bar\n---\n")
        p = self._build(
            tasks_dir, phase="execute",
            task_subset=[("bar", md)],
            task_progress=(1, 3),
        )
        assert "EXACTLY this one task" in p
        assert "0010-bar.md" in p
        assert "Make ONE commit when done" in p
        assert "call 1 of 3" in p

    def test_plan_legacy_no_subset_keeps_old_wording(self, tasks_dir: Path):
        # No task_subset → legacy "EVERY task with planned: false" wording.
        p = self._build(tasks_dir, phase="plan")
        assert "EVERY task with `planned: false`" in p

    def test_includes_already_built(self, tasks_dir: Path):
        (tasks_dir / "0001-done.md").write_text(SAMPLE_EXECUTED)
        p = self._build(tasks_dir)
        assert "ALREADY BUILT" in p
        assert "test-app-done" in p

    def test_includes_env_vars(self, tasks_dir: Path):
        p = self._build(
            tasks_dir, env_vars=["API_KEY (set in .env)"]
        )
        assert "API_KEY" in p

    def test_mandatory_visual_check(self, tasks_dir: Path):
        p = self._build(
            tasks_dir,
            dev_server_url="http://localhost:3100",
            screenshot_dir=".autobuilder/initiatives/0001-test-app/review-screenshots",
        )
        assert "screenshot" in p.lower()

    def test_browser_session_in_prompt(self, tasks_dir: Path):
        p = self._build(
            tasks_dir,
            dev_server_url="http://localhost:3100",
            screenshot_dir=".autobuilder/initiatives/0001-test-app/review-screenshots",
            browser_session="ab-test-proj",
        )
        assert "--session ab-test-proj" in p
        assert "agent-browser --session ab-test-proj close" in p

    def test_browser_close_instruction_in_prompt(self, tasks_dir: Path):
        p = self._build(
            tasks_dir,
            dev_server_url="http://localhost:3100",
            screenshot_dir=".autobuilder/initiatives/0001-test-app/review-screenshots",
        )
        assert "close" in p.lower()
        assert "done with the browser" in p.lower()

    def test_includes_initiative_spec_when_set(self, tasks_dir: Path):
        p = self._build(
            tasks_dir,
            initiative_spec="Ship the dashboard.",
            initiative_constraints="Use TypeScript.",
        )
        # No spec.md on disk → legacy inline fallback fires.
        assert "Ship the dashboard." in p
        assert "Use TypeScript." in p

    def test_uses_file_reference_when_spec_exists_on_disk(self, tasks_dir: Path):
        """When spec.md/constraints.md/PRODUCT.md exist on disk we reference
        them by path in REQUIRED READING instead of inlining the body —
        this is the per-token cost optimisation."""
        init_dir = tasks_dir.parent
        project_dir = tasks_dir.parent.parent.parent.parent
        (init_dir / "spec.md").write_text("Spec body content here.")
        (init_dir / "constraints.md").write_text("Constraint body content here.")
        (project_dir / ".autobuilder" / "PRODUCT.md").write_text(
            "Product body content here."
        )

        p = self._build(
            tasks_dir,
            initiative_spec="Spec body content here.",
            initiative_constraints="Constraint body content here.",
            project_context="Product body content here.",
        )

        # File paths must appear in REQUIRED READING (relative to project_dir).
        assert "REQUIRED READING" in p
        assert ".autobuilder/initiatives/0001-test-app/spec.md" in p
        assert ".autobuilder/initiatives/0001-test-app/constraints.md" in p
        assert ".autobuilder/PRODUCT.md" in p

        # The bodies themselves must NOT be inlined (we just point to files).
        assert "Spec body content here." not in p
        assert "Constraint body content here." not in p
        assert "Product body content here." not in p

        # And the legacy section headers must be absent.
        assert "INITIATIVE SPEC (your scope" not in p
        assert "INITIATIVE CONSTRAINTS:" not in p
        assert "PROJECT CONTEXT (legacy" not in p

    def test_legacy_inline_fires_when_only_legacy_scope_md_exists(
        self, tasks_dir: Path,
    ):
        """If no PRODUCT.md is on disk but a synthesised legacy
        ``project_context`` is supplied, we MUST inline it so the agent
        still gets the project-level background."""
        p = self._build(
            tasks_dir,
            project_context="Synthesised from scope.md + constraints.md.",
        )
        assert "Synthesised from scope.md + constraints.md." in p
        assert "PROJECT CONTEXT (legacy" in p


class TestGenerateMemoryIndex:
    def test_returns_markdown(self, tmp_path: Path):
        project_dir = tmp_path
        (project_dir / ".autobuilder").mkdir(parents=True)
        tasks_dir = (
            project_dir / ".autobuilder" / "initiatives" / "0001-app" / "tasks"
        )
        tasks_dir.mkdir(parents=True)
        initiatives_base = project_dir / ".autobuilder" / "initiatives"
        text = prompt.generate_memory_index(
            project_dir, tasks_dir, initiatives_base, root_id="app",
        )
        assert "Iteration Memory" in text


# ═══════════════════════════════════════════════════════════════════════════
# config — project registry
# ═══════════════════════════════════════════════════════════════════════════


class TestLoadProjectsConfig:
    def test_loads_config(self, tmp_path: Path):
        cfg = tmp_path / "config.json"
        cfg.write_text(json.dumps({
            "projects": [
                {"name": "my-app", "dir": "/tmp/my-app", "autobuilderDir": "/tmp/my-app/.autobuilder"},
            ]
        }))
        projects = config_mod.load_projects_config(cfg)
        assert len(projects) == 1
        assert projects[0]["name"] == "my-app"

    def test_missing_file(self, tmp_path: Path):
        assert config_mod.load_projects_config(tmp_path / "nope.json") == []

    def test_none_path(self):
        result = config_mod.load_projects_config(Path("/nonexistent/config.json"))
        assert result == []


class TestResolveProjectFromConfig:
    def _write_config(self, tmp_path: Path) -> Path:
        cfg = tmp_path / "config.json"
        cfg.write_text(json.dumps({
            "projects": [
                {
                    "name": "simple-app",
                    "dir": "/workspace/simple-app",
                    "autobuilderDir": "/workspace/simple-app/.autobuilder",
                    "port": 3000,
                },
                {
                    "name": "monorepo-app",
                    "dir": "/workspace/monorepo",
                    "autobuilderDir": "/workspace/monorepo/apps/web/.autobuilder",
                    "port": 6200,
                },
                {
                    "name": "no-port-app",
                    "dir": "/workspace/no-port",
                    "autobuilderDir": "/workspace/no-port/.autobuilder",
                },
            ]
        }))
        return cfg

    def test_resolves_simple(self, tmp_path: Path):
        cfg = self._write_config(tmp_path)
        result = config_mod.resolve_project_from_config("simple-app", cfg)
        assert result["project_dir"] == "/workspace/simple-app"
        assert result["project_id"] == "simple-app"
        assert result["root_initiative"] == "simple-app"

    def test_resolves_monorepo(self, tmp_path: Path):
        cfg = self._write_config(tmp_path)
        result = config_mod.resolve_project_from_config("monorepo-app", cfg)
        assert result["project_dir"] == "/workspace/monorepo/apps/web"
        assert result["project_id"] == "monorepo-app"

    def test_resolves_port(self, tmp_path: Path):
        cfg = self._write_config(tmp_path)
        result = config_mod.resolve_project_from_config("simple-app", cfg)
        assert result["port"] == 3000

    def test_resolves_port_monorepo(self, tmp_path: Path):
        cfg = self._write_config(tmp_path)
        result = config_mod.resolve_project_from_config("monorepo-app", cfg)
        assert result["port"] == 6200

    def test_port_none_when_missing(self, tmp_path: Path):
        cfg = self._write_config(tmp_path)
        result = config_mod.resolve_project_from_config("no-port-app", cfg)
        assert result["port"] is None

    def test_not_found_exits(self, tmp_path: Path):
        cfg = self._write_config(tmp_path)
        with pytest.raises(SystemExit):
            config_mod.resolve_project_from_config("nonexistent", cfg)


# ═══════════════════════════════════════════════════════════════════════════
# iteration — exception classes
# ═══════════════════════════════════════════════════════════════════════════


class TestExceptionHierarchy:
    def test_graceful_stop(self):
        with pytest.raises(iteration.GracefulStop):
            raise iteration.GracefulStop()

    def test_nothing_to_improve(self):
        with pytest.raises(iteration.NothingToImprove):
            raise iteration.NothingToImprove()

    def test_agent_failed(self):
        with pytest.raises(iteration.AgentFailed):
            raise iteration.AgentFailed("exit code 1")
