"""Extended tests for iteration.py: _build_initiative_map, exception metadata."""

import textwrap
from pathlib import Path

import pytest

import iteration
from conftest import SAMPLE_EXECUTED, SAMPLE_TASK, SAMPLE_UNPLANNED


# ═══════════════════════════════════════════════════════════════════════════
# _build_initiative_map
# ═══════════════════════════════════════════════════════════════════════════


def _create_initiative(project_dir: Path, name: str, tasks: list = None) -> Path:
    init_dir = project_dir / ".autobuilder" / "initiatives" / name
    (init_dir / "tasks").mkdir(parents=True)
    (init_dir / "spec.md").write_text("# Spec\n")
    if tasks:
        for task_name, content in tasks:
            (init_dir / "tasks" / task_name).write_text(content)
    return init_dir


class TestBuildInitiativeMap:
    def test_empty_when_no_initiatives(self, project_dir: Path):
        result = iteration._build_initiative_map(project_dir, "0001-app")
        assert result == ""

    def test_shows_active_with_no_tasks(self, project_dir: Path):
        _create_initiative(project_dir, "0001-app")
        result = iteration._build_initiative_map(project_dir, "0001-app")
        assert "active" in result
        assert "no tasks yet" in result
        assert "*" in result

    def test_shows_active_with_progress(self, project_dir: Path):
        _create_initiative(project_dir, "0001-app", [
            ("0001-done.md", SAMPLE_EXECUTED),
            ("0002-pending.md", SAMPLE_TASK),
        ])
        result = iteration._build_initiative_map(project_dir, "0001-app")
        assert "active" in result
        assert "1/2 executed" in result

    def test_shows_completed_initiative(self, project_dir: Path):
        _create_initiative(project_dir, "0001-old", [
            ("0001-done.md", SAMPLE_EXECUTED),
        ])
        _create_initiative(project_dir, "0002-active")
        result = iteration._build_initiative_map(project_dir, "0002-active")
        assert "completed" in result
        assert "1 tasks" in result

    def test_shows_paused_initiative(self, project_dir: Path):
        _create_initiative(project_dir, "0001-paused", [
            ("0001-done.md", SAMPLE_EXECUTED),
            ("0002-pending.md", SAMPLE_TASK),
        ])
        _create_initiative(project_dir, "0002-active")
        result = iteration._build_initiative_map(project_dir, "0002-active")
        assert "paused" in result
        assert "1/2 executed" in result

    def test_shows_multiple_initiatives(self, project_dir: Path):
        _create_initiative(project_dir, "0001-first", [
            ("0001-done.md", SAMPLE_EXECUTED),
        ])
        _create_initiative(project_dir, "0002-second")
        result = iteration._build_initiative_map(project_dir, "0002-second")
        assert "0001-first" in result
        assert "0002-second" in result


# ═══════════════════════════════════════════════════════════════════════════
# Exception classes with journal_data
# ═══════════════════════════════════════════════════════════════════════════


class TestExceptionJournalData:
    def test_nothing_to_improve_carries_data(self):
        data = {"iter": 5, "strategy": "deep-dive"}
        exc = iteration.NothingToImprove(journal_data=data)
        assert exc.journal_data["iter"] == 5
        assert exc.journal_data["strategy"] == "deep-dive"

    def test_nothing_to_improve_default_empty(self):
        exc = iteration.NothingToImprove()
        assert exc.journal_data == {}

    def test_agent_failed_carries_data(self):
        data = {"iter": 3, "rc": 1}
        exc = iteration.AgentFailed("exit 1", journal_data=data)
        assert exc.journal_data["iter"] == 3
        assert str(exc) == "exit 1"

    def test_agent_failed_default_empty(self):
        exc = iteration.AgentFailed("error")
        assert exc.journal_data == {}

    def test_graceful_stop(self):
        exc = iteration.GracefulStop()
        assert isinstance(exc, Exception)


# ═══════════════════════════════════════════════════════════════════════════
# Initiative map edge cases
# ═══════════════════════════════════════════════════════════════════════════


class TestBuildInitiativeMapEdgeCases:
    def test_empty_initiative_label(self, project_dir: Path):
        init_dir = project_dir / ".autobuilder" / "initiatives" / "0001-empty"
        init_dir.mkdir(parents=True)
        (init_dir / "spec.md").write_text("# Spec\n")
        result = iteration._build_initiative_map(project_dir, "9999-other")
        assert "empty" in result

    def test_initiative_with_no_tasks_dir(self, project_dir: Path):
        init_dir = project_dir / ".autobuilder" / "initiatives" / "0001-notasks"
        init_dir.mkdir(parents=True)
        (init_dir / "spec.md").write_text("# Spec\n")
        result = iteration._build_initiative_map(project_dir, "0001-notasks")
        assert "no tasks yet" in result


# ═══════════════════════════════════════════════════════════════════════════
# _chunk + _expand_phase_into_calls — per-task fanout
# ═══════════════════════════════════════════════════════════════════════════


class TestChunk:
    def test_size_one_means_one_per_chunk(self):
        assert iteration._chunk(["a", "b", "c"], 1) == [["a"], ["b"], ["c"]]

    def test_partial_last_chunk(self):
        assert iteration._chunk(["a", "b", "c", "d", "e"], 2) == [
            ["a", "b"], ["c", "d"], ["e"],
        ]

    def test_empty_input(self):
        assert iteration._chunk([], 1) == []
        assert iteration._chunk([], 5) == []

    def test_size_zero_returns_single_chunk(self):
        assert iteration._chunk(["a", "b"], 0) == [["a", "b"]]


def _write_task(tasks_dir: Path, name: str, body: str) -> None:
    (tasks_dir / name).write_text(body)


class TestExpandPhaseIntoCalls:
    def test_review_always_one_call(self, tasks_dir: Path):
        cfg = {"plan_batch_size": 1, "execute_batch_size": 1}
        out = iteration._expand_phase_into_calls(
            phase="review", project_config=cfg, tasks_dir=tasks_dir,
        )
        assert out == [None]

    def test_plan_per_task_default(self, tasks_dir: Path):
        # Three unplanned tasks → three calls by default.
        for n in ("0010-a.md", "0011-b.md", "0012-c.md"):
            _write_task(tasks_dir, n, SAMPLE_UNPLANNED.replace(
                "test-app-charts", n[:-3]
            ))
        cfg = {"plan_batch_size": 1, "execute_batch_size": 1}
        out = iteration._expand_phase_into_calls(
            phase="plan", project_config=cfg, tasks_dir=tasks_dir,
        )
        assert len(out) == 3
        for sub in out:
            assert sub is not None
            assert len(sub) == 1

    def test_plan_batch_size_3_groups_tasks(self, tasks_dir: Path):
        for n in ("0010-a.md", "0011-b.md", "0012-c.md", "0013-d.md", "0014-e.md"):
            _write_task(tasks_dir, n, SAMPLE_UNPLANNED.replace(
                "test-app-charts", n[:-3]
            ))
        cfg = {"plan_batch_size": 3, "execute_batch_size": 1}
        out = iteration._expand_phase_into_calls(
            phase="plan", project_config=cfg, tasks_dir=tasks_dir,
        )
        assert [len(s) for s in out] == [3, 2]

    def test_plan_null_keeps_legacy_single_call(self, tasks_dir: Path):
        for n in ("0010-a.md", "0011-b.md"):
            _write_task(tasks_dir, n, SAMPLE_UNPLANNED.replace(
                "test-app-charts", n[:-3]
            ))
        cfg = {"plan_batch_size": None, "execute_batch_size": None}
        out = iteration._expand_phase_into_calls(
            phase="plan", project_config=cfg, tasks_dir=tasks_dir,
        )
        assert out == [None]

    def test_plan_skipped_when_no_unplanned(self, tasks_dir: Path):
        cfg = {"plan_batch_size": 1, "execute_batch_size": 1}
        out = iteration._expand_phase_into_calls(
            phase="plan", project_config=cfg, tasks_dir=tasks_dir,
        )
        assert out == []

    def test_execute_per_task_topological(self, tasks_dir: Path):
        # b depends on a → a must come first when fanned out.
        a = SAMPLE_TASK.replace("test-app-scaffold", "alpha")
        b = SAMPLE_TASK.replace("test-app-scaffold", "beta") \
            .replace("deps: []", "deps: [alpha]")
        _write_task(tasks_dir, "0010-alpha.md", a)
        _write_task(tasks_dir, "0011-beta.md", b)
        cfg = {"plan_batch_size": 1, "execute_batch_size": 1}
        out = iteration._expand_phase_into_calls(
            phase="execute", project_config=cfg, tasks_dir=tasks_dir,
        )
        assert len(out) == 2
        # First sub-call should plan alpha (the dep), second beta.
        first_ids = [tid for tid, _ in out[0]]
        second_ids = [tid for tid, _ in out[1]]
        assert first_ids == ["alpha"]
        assert second_ids == ["beta"]

    def test_execute_skipped_when_nothing_pending(self, tasks_dir: Path):
        _write_task(tasks_dir, "0010-done.md", SAMPLE_EXECUTED)
        cfg = {"plan_batch_size": 1, "execute_batch_size": 1}
        out = iteration._expand_phase_into_calls(
            phase="execute", project_config=cfg, tasks_dir=tasks_dir,
        )
        assert out == []
