"""Tests for D1-D4: CI mode, action.yml, and combined pipeline workflow."""

import json
import subprocess
import sys
from pathlib import Path

import pytest
import yaml
from click.testing import CliRunner

from agentreflect.cli import cli

REPO_ROOT = Path(__file__).parent.parent


# ---------------------------------------------------------------------------
# D2: CI mode flag tests
# ---------------------------------------------------------------------------


def _ci_invoke(args):
    """Invoke CLI and parse JSON from output (CliRunner mixes stdout/stderr)."""
    runner = CliRunner()
    return runner.invoke(cli, args)


def _parse_ci_json(output):
    """Extract and parse the JSON object from CI mode output (which may include stderr lines)."""
    # Find the JSON block: starts with '{' and ends with the matching '}'
    start = output.find('{')
    if start == -1:
        raise ValueError(f"No JSON found in output: {output!r}")
    # Find matching closing brace using depth counting
    depth = 0
    end = start
    for i, ch in enumerate(output[start:], start):
        if ch == '{':
            depth += 1
        elif ch == '}':
            depth -= 1
            if depth == 0:
                end = i + 1
                break
    return json.loads(output[start:end])


class TestCIMode:
    """Test --ci flag on generate subcommand."""

    def test_ci_mode_outputs_json_to_stdout(self, tmp_path):
        """CI mode should write JSON to stdout."""
        result = _ci_invoke(["generate", "--from-notes", "test failed", "--ci"])
        assert result.exit_code == 0
        data = _parse_ci_json(result.output)
        assert data["ci"] is True

    def test_ci_mode_includes_rules_count(self, tmp_path):
        """CI mode JSON must include rules_count int field."""
        result = _ci_invoke(["generate", "--from-notes", "test failed", "--ci"])
        assert result.exit_code == 0
        data = _parse_ci_json(result.output)
        assert "rules_count" in data
        assert isinstance(data["rules_count"], int)

    def test_ci_mode_includes_suggestions_list(self):
        """CI mode JSON includes suggestions array."""
        result = _ci_invoke(["generate", "--from-notes", "test failed", "--ci"])
        assert result.exit_code == 0
        data = _parse_ci_json(result.output)
        assert "suggestions" in data
        assert isinstance(data["suggestions"], list)

    def test_ci_mode_suggestion_fields(self):
        """Each suggestion has rule, rationale, confidence, category."""
        result = _ci_invoke(["generate", "--from-notes", "import failed", "--ci"])
        assert result.exit_code == 0
        data = _parse_ci_json(result.output)
        if data["suggestions"]:
            s = data["suggestions"][0]
            assert "rule" in s or "text" in s  # rule or text key
            assert "confidence" in s
            assert "category" in s

    def test_ci_mode_exit_0_when_no_fail_on(self):
        """CI mode without --fail-on always exits 0 regardless of rule count."""
        result = _ci_invoke(["generate", "--from-notes", "xyz123abc", "--ci"])
        assert result.exit_code == 0
        data = _parse_ci_json(result.output)
        assert isinstance(data["rules_count"], int)

    def test_fail_on_zero_always_exits_0(self):
        """--fail-on 0 should never cause exit code 1."""
        result = _ci_invoke(["generate", "--from-notes", "test failed", "--ci", "--fail-on", "0"])
        assert result.exit_code == 0

    def test_fail_on_threshold_not_met_exits_0(self):
        """--fail-on N where N > rules_count should exit 0."""
        result = _ci_invoke(
            ["generate", "--from-notes", "xyz no pattern", "--ci", "--fail-on", "999"]
        )
        assert result.exit_code == 0

    def test_fail_on_threshold_met_exits_1(self):
        """--fail-on 1 when rules_count >= 1 should exit 1."""
        result = _ci_invoke(
            ["generate", "--from-notes", "import failed again", "--ci", "--fail-on", "1"]
        )
        # If any rules are generated, expect exit 1; if 0 rules, exit 0
        data = _parse_ci_json(result.output)
        if data["rules_count"] >= 1:
            assert result.exit_code == 1
        else:
            assert result.exit_code == 0

    def test_ci_mode_does_not_output_markdown(self):
        """CI mode should output JSON, not markdown."""
        result = _ci_invoke(["generate", "--from-notes", "test failed", "--ci"])
        assert result.exit_code == 0
        data = _parse_ci_json(result.output)
        assert isinstance(data, dict)
        assert not result.output.startswith("#")

    def test_ci_mode_source_field_present(self):
        """CI mode JSON includes source field."""
        result = _ci_invoke(["generate", "--from-notes", "test failed", "--ci"])
        assert result.exit_code == 0
        data = _parse_ci_json(result.output)
        assert "source" in data

    def test_ci_mode_with_pytest_file(self, tmp_path):
        """CI mode works with --from-pytest input."""
        failures_file = tmp_path / "failures.txt"
        failures_file.write_text(
            "FAILED tests/test_foo.py::test_bar - AssertionError\n"
        )
        result = _ci_invoke(
            ["generate", "--from-pytest", str(failures_file), "--ci"]
        )
        assert result.exit_code == 0
        data = _parse_ci_json(result.output)
        assert data["ci"] is True
        assert isinstance(data["rules_count"], int)

    def test_fail_on_5_exits_0_when_fewer_than_5_rules(self):
        """--fail-on 5 should exit 0 when rules_count < 5."""
        result = _ci_invoke(
            ["generate", "--from-notes", "xyz no pattern", "--ci", "--fail-on", "5"]
        )
        data = _parse_ci_json(result.output)
        if data["rules_count"] < 5:
            assert result.exit_code == 0

    def test_ci_mode_no_stderr_json_noise(self):
        """CI mode JSON goes to stdout only; stderr is separate."""
        result = _ci_invoke(["generate", "--from-notes", "test failed", "--ci"])
        data = _parse_ci_json(result.output)
        assert data["ci"] is True

    def test_ci_mode_rules_count_matches_suggestions_length(self):
        """rules_count must equal len(suggestions)."""
        result = _ci_invoke(["generate", "--from-notes", "import failed", "--ci"])
        assert result.exit_code == 0
        data = _parse_ci_json(result.output)
        assert data["rules_count"] == len(data["suggestions"])


# ---------------------------------------------------------------------------
# D1: action.yml tests
# ---------------------------------------------------------------------------


class TestActionYml:
    """Validate action.yml structure."""

    @pytest.fixture(scope="class")
    def action(self):
        action_path = REPO_ROOT / "action.yml"
        assert action_path.exists(), "action.yml must exist at repo root"
        with open(action_path) as f:
            return yaml.safe_load(f)

    def test_action_yml_is_valid_yaml(self, action):
        assert action is not None
        assert isinstance(action, dict)

    def test_action_has_name(self, action):
        assert "name" in action

    def test_action_has_description(self, action):
        assert "description" in action

    def test_action_has_branding(self, action):
        assert "branding" in action
        assert action["branding"]["icon"] == "refresh-cw"
        assert action["branding"]["color"] == "purple"

    def test_action_has_required_inputs(self, action):
        inputs = action.get("inputs", {})
        assert "mode" in inputs
        assert "from-pytest" in inputs
        assert "from-git" in inputs
        assert "threshold" in inputs
        assert "output-format" in inputs

    def test_action_threshold_default_is_0(self, action):
        threshold = action["inputs"]["threshold"]
        assert str(threshold.get("default", "")) == "0"

    def test_action_has_required_outputs(self, action):
        outputs = action.get("outputs", {})
        assert "rules-generated" in outputs
        assert "suggestions-path" in outputs

    def test_action_has_runs(self, action):
        assert "runs" in action
        assert action["runs"]["using"] == "composite"

    def test_action_has_steps(self, action):
        steps = action["runs"].get("steps", [])
        assert len(steps) >= 1


# ---------------------------------------------------------------------------
# D3: Combined pipeline workflow tests
# ---------------------------------------------------------------------------


class TestPipelineWorkflow:
    """Validate combined quartet pipeline workflow."""

    @pytest.fixture(scope="class")
    def pipeline(self):
        pipeline_path = (
            REPO_ROOT / ".github" / "workflows" / "examples" / "agent-quality-pipeline.yml"
        )
        assert pipeline_path.exists(), "agent-quality-pipeline.yml must exist"
        with open(pipeline_path) as f:
            return yaml.safe_load(f)

    def test_pipeline_is_valid_yaml(self, pipeline):
        assert pipeline is not None
        assert isinstance(pipeline, dict)

    def test_pipeline_triggers_on_pull_request(self, pipeline):
        # PyYAML parses YAML `on:` key as Python boolean True
        on = pipeline.get("on", pipeline.get(True, {}))
        assert "pull_request" in on

    def test_pipeline_has_4_jobs(self, pipeline):
        jobs = pipeline.get("jobs", {})
        assert len(jobs) == 4

    def test_pipeline_has_agentlint_job(self, pipeline):
        jobs = pipeline.get("jobs", {})
        assert "agentlint-check" in jobs

    def test_pipeline_has_agentmd_job(self, pipeline):
        jobs = pipeline.get("jobs", {})
        assert "agentmd-drift" in jobs

    def test_pipeline_has_coderace_job(self, pipeline):
        jobs = pipeline.get("jobs", {})
        assert "coderace-review" in jobs

    def test_pipeline_has_agentreflect_job(self, pipeline):
        jobs = pipeline.get("jobs", {})
        assert "agentreflect-learn" in jobs

    def test_pipeline_jobs_run_in_order(self, pipeline):
        """Each job should depend on the previous."""
        jobs = pipeline.get("jobs", {})
        agentmd_needs = jobs.get("agentmd-drift", {}).get("needs", [])
        coderace_needs = jobs.get("coderace-review", {}).get("needs", [])
        agentreflect_needs = jobs.get("agentreflect-learn", {}).get("needs", [])
        assert len(agentmd_needs) >= 1
        assert len(coderace_needs) >= 1
        assert len(agentreflect_needs) >= 1

    def test_pipeline_has_full_pipeline_comment(self, pipeline_raw=None):
        """Pipeline file should have the required comment."""
        pipeline_path = (
            REPO_ROOT / ".github" / "workflows" / "examples" / "agent-quality-pipeline.yml"
        )
        content = pipeline_path.read_text()
        assert "MEASURE" in content or "GENERATE" in content or "GUARD" in content or "LEARN" in content
