"""Tests for cleave workspace module - workspace generation and context reconstruction."""

from __future__ import annotations

from pathlib import Path

import pytest

from cleave.core.workspace import (
    MAX_ALLOWED_DEPTH,
    generate_intent_scaffold,
    generate_manifest,
    generate_siblings_yaml,
    generate_task_file,
    generate_workspace_name,
    init_workspace,
    reconstruct_context,
    validate_intent_completeness,
)
from cleave.core.yaml_utils import parse_yaml_simple


class TestMaxAllowedDepth:
    """Tests for max_depth bounds checking."""

    def test_max_allowed_depth_is_10(self) -> None:
        """MAX_ALLOWED_DEPTH should be 10."""
        assert MAX_ALLOWED_DEPTH == 10

    def test_inherited_max_depth_capped(self, tmp_path: Path) -> None:
        """Inherited max_depth should be capped at MAX_ALLOWED_DEPTH."""
        # Create parent manifest with excessive max_depth
        parent_dir = tmp_path / "parent"
        parent_dir.mkdir()
        parent_manifest = parent_dir / "manifest.yaml"
        parent_manifest.write_text("""
max_depth: 1000
intent:
  goal: "Test goal"
ancestry:
  depth: 0
  node_id: root
""")

        # Create child workspace
        child_dir = tmp_path / "child"
        result = init_workspace(
            directive="Test task",
            children=["A", "B"],
            output_dir=str(child_dir),
            parent_manifest_path=str(parent_manifest),
            depth=1,
        )

        # Should not error, and max_depth should be capped
        assert "error" not in result

        # Read the generated manifest and verify max_depth is capped
        child_manifest = child_dir / "manifest.yaml"
        content = parse_yaml_simple(child_manifest.read_text())
        assert content["max_depth"] <= MAX_ALLOWED_DEPTH


class TestGenerateIntentScaffold:
    """Tests for intent scaffold generation."""

    def test_generates_pattern_scaffold(self) -> None:
        """Should generate pattern-specific scaffold when pattern matches."""
        scaffold = generate_intent_scaffold("Add user auth", "authentication")
        assert len(scaffold["success_criteria"]) >= 3
        assert len(scaffold["constraints"]) >= 1
        # Should not have TODO placeholders
        assert not any("TODO" in c for c in scaffold["success_criteria"])

    def test_generates_generic_scaffold(self) -> None:
        """Should generate TODO placeholders when no pattern matches."""
        scaffold = generate_intent_scaffold("Do something custom", None)
        assert any("TODO" in c for c in scaffold["success_criteria"])

    def test_truncates_long_goals(self) -> None:
        """Should truncate goals longer than 100 characters."""
        long_directive = "A" * 150
        scaffold = generate_intent_scaffold(long_directive, None)
        assert len(scaffold["goal"]) <= 103  # 100 + "..."


class TestGenerateManifest:
    """Tests for manifest.yaml generation."""

    def test_generates_valid_yaml(self) -> None:
        """Should generate parseable YAML."""
        content = generate_manifest(
            directive="Add authentication",
            children=["Backend", "Frontend"],
        )
        manifest = parse_yaml_simple(content)
        assert manifest["version"] == 1
        assert len(manifest["children"]) == 2

    def test_includes_assessment(self) -> None:
        """Should include complexity assessment."""
        content = generate_manifest(
            directive="Add JWT authentication with password hashing",
            children=["Auth"],
        )
        manifest = parse_yaml_simple(content)
        assert "assessment" in manifest
        assert "complexity" in manifest["assessment"]
        assert "method" in manifest["assessment"]

    def test_includes_ancestry(self) -> None:
        """Should include ancestry information."""
        content = generate_manifest(
            directive="Test",
            children=["A"],
            depth=0,
            node_id="root",
        )
        manifest = parse_yaml_simple(content)
        assert manifest["ancestry"]["depth"] == 0
        assert manifest["ancestry"]["node_id"] == "root"

    def test_inherits_intent_from_parent(self) -> None:
        """Should inherit intent from parent manifest (immutable)."""
        parent_manifest = {
            "intent": {
                "goal": "Parent goal",
                "success_criteria": ["Parent criterion 1"],
                "constraints": ["Parent constraint"],
                "out_of_scope": ["Parent exclusion"],
            },
            "ancestry": {
                "depth": 0,
                "node_id": "root",
            },
            "root_directive": "Parent directive",
        }
        content = generate_manifest(
            directive="Child task",
            children=["A"],
            parent_manifest=parent_manifest,
            depth=1,
        )
        manifest = parse_yaml_simple(content)
        # Intent should be inherited verbatim
        assert manifest["intent"]["goal"] == "Parent goal"

    def test_includes_permissions_when_requested(self) -> None:
        """Should include inferred permissions when flag is set."""
        content = generate_manifest(
            directive="Build Python API with pytest",
            children=["API"],
            include_permissions=True,
        )
        manifest = parse_yaml_simple(content)
        assert "inferred_permissions" in manifest
        assert "bundles" in manifest["inferred_permissions"]


class TestGenerateTaskFile:
    """Tests for task file generation (slim template)."""

    def test_generates_task_with_tdd(self) -> None:
        """Should include TDD marker by default."""
        content = generate_task_file(
            task_id=0,
            label="Backend API",
            mission="Implement the API endpoints",
            siblings=["Backend API", "Frontend UI"],
            tdd=True,
        )
        # Slim template uses compact TDD marker
        assert "tdd: true" in content
        assert "Red→Green→Refactor" in content

    def test_generates_task_without_tdd(self) -> None:
        """Should not include TDD marker when disabled."""
        content = generate_task_file(
            task_id=0,
            label="Backend API",
            mission="Implement the API endpoints",
            siblings=["Backend API"],
            tdd=False,
        )
        assert "tdd: false" in content
        assert "Red→Green→Refactor" not in content
        # Should still have verification section
        assert "Verification" in content

    def test_includes_sibling_references(self) -> None:
        """Should reference sibling tasks in compact format."""
        content = generate_task_file(
            task_id=0,
            label="Task A",
            mission="Do A",
            siblings=["Task A", "Task B", "Task C"],
        )
        # Slim template uses compact format: [1:Task B, 2:Task C]
        assert "1:Task B" in content
        assert "2:Task C" in content

    def test_includes_result_template(self) -> None:
        """Should include slim result template with core fields."""
        content = generate_task_file(
            task_id=0,
            label="Test",
            mission="Test mission",
            siblings=["Test"],
        )
        assert "**Status:** PENDING" in content
        assert "**Summary:**" in content
        assert "**Artifacts:**" in content
        assert "**Decisions:**" in content  # Slim name
        assert "**Verification:**" in content
        # Updated: now have dedicated section instead of inline
        assert "## Alignment Check" in content


class TestGenerateSiblingsYaml:
    """Tests for siblings.yaml generation."""

    def test_generates_sibling_entries(self) -> None:
        """Should generate entry for each sibling."""
        content = generate_siblings_yaml(["Backend", "Frontend", "Database"])
        data = parse_yaml_simple(content)
        assert len(data["siblings"]) == 3
        assert data["siblings"][0]["label"] == "Backend"
        assert data["siblings"][1]["label"] == "Frontend"
        assert data["siblings"][2]["label"] == "Database"

    def test_siblings_start_pending(self) -> None:
        """All siblings should start with pending status."""
        content = generate_siblings_yaml(["A", "B"])
        data = parse_yaml_simple(content)
        for sibling in data["siblings"]:
            assert sibling["status"] == "pending"

    def test_siblings_have_coordination_metadata(self) -> None:
        """Should include coordination metadata."""
        content = generate_siblings_yaml(["A"])
        data = parse_yaml_simple(content)
        assert "coordination" in data
        assert data["coordination"]["mode"] == "parallel"


class TestInitWorkspace:
    """Tests for workspace initialization."""

    def test_creates_workspace_directory(self, tmp_path: Path) -> None:
        """Should create output directory if it doesn't exist."""
        output_dir = tmp_path / "new_workspace"
        result = init_workspace(
            directive="Test",
            children=["A", "B"],
            output_dir=str(output_dir),
        )
        assert output_dir.exists()
        assert "error" not in result

    def test_creates_all_required_files(self, tmp_path: Path) -> None:
        """Should create manifest, siblings, task files, and metrics."""
        output_dir = tmp_path / ".cleave"
        result = init_workspace(
            directive="Test",
            children=["A", "B", "C"],
            output_dir=str(output_dir),
        )
        assert "manifest.yaml" in result
        assert "siblings.yaml" in result
        assert "0-task.md" in result
        assert "1-task.md" in result
        assert "2-task.md" in result
        assert "metrics.yaml" in result

    def test_errors_at_max_depth(self, tmp_path: Path) -> None:
        """Should error when trying to cleave at max_depth."""
        result = init_workspace(
            directive="Test",
            children=["A"],
            output_dir=str(tmp_path / ".cleave"),
            max_depth=3,
            depth=3,  # Already at max
        )
        assert "error" in result
        assert result["error"] == "MAX_DEPTH_EXCEEDED"

    def test_errors_when_inherited_depth_exceeded(self, tmp_path: Path) -> None:
        """Should error when inherited max_depth would be exceeded."""
        # Create parent at depth 2 with max_depth 3
        parent_dir = tmp_path / "parent"
        parent_dir.mkdir()
        parent_manifest = parent_dir / "manifest.yaml"
        parent_manifest.write_text("""
max_depth: 3
intent:
  goal: "Test"
ancestry:
  depth: 2
  node_id: "0.1"
""")

        result = init_workspace(
            directive="Child task",
            children=["A"],
            output_dir=str(tmp_path / "child"),
            parent_manifest_path=str(parent_manifest),
            depth=3,  # Would exceed inherited max_depth
        )
        assert "error" in result
        assert result["error"] == "MAX_DEPTH_EXCEEDED"

    def test_files_are_written_atomically(self, tmp_path: Path) -> None:
        """Files should be written using atomic write pattern."""
        # This is hard to test directly, but we can verify files exist
        # and contain expected content
        output_dir = tmp_path / ".cleave"
        init_workspace(
            directive="Test atomic writes",
            children=["A"],
            output_dir=str(output_dir),
        )

        manifest = output_dir / "manifest.yaml"
        assert manifest.exists()
        content = manifest.read_text()
        assert "Test atomic writes" in content


class TestReconstructContext:
    """Tests for context reconstruction from manifest."""

    def test_reconstructs_basic_context(self, tmp_path: Path) -> None:
        """Should reconstruct context from manifest."""
        workspace = tmp_path / ".cleave"
        workspace.mkdir()
        manifest = workspace / "manifest.yaml"
        manifest.write_text("""
intent:
  goal: "Build API"
  success_criteria:
    - "Endpoints work"
  constraints:
    - "Use REST"
  out_of_scope: []
ancestry:
  depth: 0
  node_id: root
  parent_chain: []
assessment:
  complexity: 3.0
children:
  - id: 0
    label: Backend
""")

        context = reconstruct_context(str(manifest))
        assert context["root_intent"]["goal"] == "Build API"
        assert context["depth"] == 0
        assert context["current_node"] == "root"

    def test_includes_siblings_if_present(self, tmp_path: Path) -> None:
        """Should include siblings.yaml content if present."""
        workspace = tmp_path / ".cleave"
        workspace.mkdir()
        (workspace / "manifest.yaml").write_text("""
intent:
  goal: "Test"
ancestry:
  depth: 0
  node_id: root
""")
        (workspace / "siblings.yaml").write_text("""
siblings:
  - id: 0
    label: A
    status: pending
""")

        context = reconstruct_context(str(workspace / "manifest.yaml"))
        assert "siblings" in context
        assert context["siblings"]["siblings"][0]["label"] == "A"

    def test_returns_error_for_missing_manifest(self) -> None:
        """Should return error for missing manifest."""
        context = reconstruct_context("/nonexistent/manifest.yaml")
        assert "error" in context


class TestGenerateWorkspaceName:
    """Tests for automatic workspace name generation."""

    def test_generates_slug_from_directive(self, tmp_path: Path) -> None:
        """Should create slug from directive."""
        name = generate_workspace_name("Add JWT authentication", str(tmp_path))
        assert name == ".cleave-add-jwt-authentication"

    def test_truncates_long_directives(self, tmp_path: Path) -> None:
        """Should truncate directives longer than 40 chars."""
        long_directive = "Add authentication system with JWT tokens and refresh token rotation"
        name = generate_workspace_name(long_directive, str(tmp_path))
        assert len(name) <= 48  # ".cleave-" (8) + 40
        assert name.startswith(".cleave-add-authentication-system")

    def test_removes_special_characters(self, tmp_path: Path) -> None:
        """Should remove special chars and replace spaces with hyphens."""
        name = generate_workspace_name("Add @user-auth (JWT & OAuth2.0)", str(tmp_path))
        assert "@" not in name
        assert "&" not in name
        assert "(" not in name
        assert ")" not in name
        assert " " not in name
        assert name == ".cleave-add-user-auth-jwt-oauth20"

    def test_adds_suffix_for_existing_workspace(self, tmp_path: Path) -> None:
        """Should add numeric suffix if workspace already exists."""
        # Create first workspace
        (tmp_path / ".cleave-add-auth").mkdir()

        name = generate_workspace_name("Add auth", str(tmp_path))
        assert name == ".cleave-add-auth-2"

    def test_finds_next_available_suffix(self, tmp_path: Path) -> None:
        """Should find next available suffix when multiple exist."""
        (tmp_path / ".cleave-add-auth").mkdir()
        (tmp_path / ".cleave-add-auth-2").mkdir()
        (tmp_path / ".cleave-add-auth-3").mkdir()

        name = generate_workspace_name("Add auth", str(tmp_path))
        assert name == ".cleave-add-auth-4"

    def test_collapses_multiple_hyphens(self, tmp_path: Path) -> None:
        """Should collapse multiple hyphens into one."""
        name = generate_workspace_name("Add --- multiple     spaces", str(tmp_path))
        assert "---" not in name
        assert name == ".cleave-add-multiple-spaces"

    def test_strips_leading_trailing_hyphens(self, tmp_path: Path) -> None:
        """Should strip leading/trailing hyphens from slug."""
        name = generate_workspace_name("---Add auth---", str(tmp_path))
        assert not name.endswith("---")
        assert name == ".cleave-add-auth"


class TestInitWorkspaceAutoNaming:
    """Tests for init_workspace with automatic naming."""

    def test_auto_generates_workspace_name(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """Should auto-generate workspace name when output_dir is None."""
        # Run in isolated tmp_path to avoid collision with other tests
        monkeypatch.chdir(tmp_path)

        result = init_workspace(
            directive="Add JWT authentication",
            children=["Backend", "Frontend"],
            output_dir=None,
        )

        assert "workspace" in result
        assert result["workspace"] == ".cleave-add-jwt-authentication"
        assert Path(result["workspace"]).exists()

    def test_returns_workspace_in_created_dict(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """Should return workspace path in created dict."""
        monkeypatch.chdir(tmp_path)

        result = init_workspace(
            directive="Test directive",
            children=["A", "B"],
            output_dir=None,
        )

        assert "workspace" in result
        assert isinstance(result["workspace"], str)
        assert result["workspace"].startswith(".cleave-")

    def test_honors_explicit_output_dir(self, tmp_path: Path) -> None:
        """Should use explicit output_dir when provided."""
        explicit_dir = str(tmp_path / "custom-workspace")
        result = init_workspace(
            directive="Test directive",
            children=["A", "B"],
            output_dir=explicit_dir,
        )

        assert result["workspace"] == explicit_dir
        assert Path(explicit_dir).exists()


class TestIntentValidation:
    """Tests for intent validation in init_workspace."""

    def test_init_warns_on_todo_success_criteria(self, tmp_path: Path, capsys) -> None:
        """Should warn when success criteria contain TODOs."""
        workspace = tmp_path / ".cleave"
        result = init_workspace(
            directive="Custom task with no pattern match",
            children=["A", "B"],
            output_dir=str(workspace),
            validate_intent=True,
        )

        # Check that workspace was created (warning doesn't block)
        assert "error" not in result
        assert workspace.exists()

        # Check for warning in stderr
        captured = capsys.readouterr()
        assert "WARNING" in captured.err or "TODO" in captured.err

    def test_init_no_warning_with_pattern_match(self, tmp_path: Path, capsys) -> None:
        """Should not warn when pattern provides complete intent scaffold."""
        workspace = tmp_path / ".cleave"
        result = init_workspace(
            directive="Add JWT authentication with password hashing",
            children=["Backend", "Frontend"],
            output_dir=str(workspace),
            validate_intent=True,
        )

        # Should succeed without warnings
        assert "error" not in result

        captured = capsys.readouterr()
        # Pattern-matched intents should not have TODOs
        assert "TODO" not in captured.err

    def test_init_blocks_on_incomplete_intent_with_flag(self, tmp_path: Path) -> None:
        """With --require-complete-intent, should error on TODOs."""
        workspace = tmp_path / ".cleave"
        result = init_workspace(
            directive="Custom task with no pattern match",
            children=["A"],
            output_dir=str(workspace),
            require_complete_intent=True,
        )

        # Should return error dict, not create workspace
        assert "error" in result
        assert result["error"] == "INCOMPLETE_INTENT"
        assert "TODO" in result["message"]

    def test_init_succeeds_with_require_flag_and_pattern(self, tmp_path: Path) -> None:
        """With --require-complete-intent, pattern-matched intents should pass."""
        workspace = tmp_path / ".cleave"
        result = init_workspace(
            directive="Add JWT authentication with bcrypt hashing",
            children=["Backend"],
            output_dir=str(workspace),
            require_complete_intent=True,
        )

        # Should succeed - authentication pattern has complete intent
        assert "error" not in result
        assert workspace.exists()


class TestTaskTemplateEnhancements:
    """Tests for task template improvements (alignment, context reminders)."""

    def test_task_file_includes_alignment_check_section(self) -> None:
        """Task file should have explicit alignment check section."""
        content = generate_task_file(
            task_id=0,
            label="Test Task",
            mission="Implement feature",
            siblings=["Test Task"],
        )

        # Should have dedicated alignment check section
        assert "## Alignment Check" in content
        # Should have required marker
        assert "Required" in content or "REQUIRED" in content
        # Should reference root goal
        assert "Root Goal" in content

    def test_task_file_includes_context_files_section(self) -> None:
        """Task file should remind to check context files."""
        content = generate_task_file(
            task_id=0,
            label="Test Task",
            mission="Implement feature",
            siblings=["Test Task", "Other Task"],
        )

        # Should have context files section at top
        assert "## Context Files" in content
        # Should reference manifest.yaml and siblings.yaml
        assert "manifest.yaml" in content
        assert "siblings.yaml" in content
        # Should have checkboxes for review
        assert "[ ]" in content

    def test_alignment_section_includes_verification_checklist(self) -> None:
        """Alignment section should have verification checklist."""
        content = generate_task_file(
            task_id=0,
            label="Test Task",
            mission="Implement feature",
            siblings=["Test Task"],
        )

        # Should prompt for specific checks
        assert "Success Criteria" in content
        assert "Constraints" in content
        # Should require explanation
        assert "Alignment Summary" in content or "REQUIRED" in content

    def test_context_section_appears_before_mission(self) -> None:
        """Context Files section should appear before Mission."""
        content = generate_task_file(
            task_id=0,
            label="Test Task",
            mission="Implement feature",
            siblings=["Test Task"],
        )

        context_pos = content.find("## Context Files")
        mission_pos = content.find("## Mission")

        assert context_pos < mission_pos, "Context Files should appear before Mission"


class TestIntentValidation:
    """Tests for intent validation empty fields (HIGH-2)."""

    def test_intent_validation_catches_empty_lists(self) -> None:
        """Empty success_criteria should fail validation."""
        intent = {
            "goal": "Build API",
            "success_criteria": [],
            "constraints": [],
            "out_of_scope": []
        }
        is_complete, todo_fields = validate_intent_completeness(intent)
        assert not is_complete, "Should fail validation with empty lists"
        assert "success_criteria" in todo_fields, "Should flag empty success_criteria"

    def test_intent_validation_requires_nonempty_goal(self) -> None:
        """Empty goal should fail validation."""
        intent = {
            "goal": "",
            "success_criteria": ["Works"],
            "constraints": ["Fast"],
            "out_of_scope": []
        }
        is_complete, todo_fields = validate_intent_completeness(intent)
        assert not is_complete, "Should fail validation with empty goal"
        assert "goal" in todo_fields, "Should flag empty goal"

    def test_intent_validation_passes_with_complete_fields(self) -> None:
        """Complete intent should pass validation."""
        intent = {
            "goal": "Build a REST API",
            "success_criteria": ["API responds to requests"],
            "constraints": ["Use existing database"],
            "out_of_scope": []
        }
        is_complete, todo_fields = validate_intent_completeness(intent)
        assert is_complete, "Should pass validation with complete fields"
        assert len(todo_fields) == 0, "Should have no TODO fields"
