"""request_provisioning is the pre-push gate's HTTP-facing half (see
resolve_codegen_capabilities in workers/tasks/codegen.py for the arq-job half). arq's
result_info() only distinguishes job success/failure by whether the job raised, never
by inspecting its return value -- so a required primitive_gap (not a real error) has
to be raised as a specific, recognized PrimitiveGapError for this function to tell it
apart from a genuine provisioning failure, both of which are arq job failures."""
from __future__ import annotations

from types import SimpleNamespace
from unittest.mock import AsyncMock
from uuid import uuid4

import pytest
from src.domain.codegen.provision_gate import PrimitiveGapError, request_provisioning


def _repo(run):
    return SimpleNamespace(
        get_by_id=AsyncMock(return_value=run),
        get_project_discovery=AsyncMock(return_value=None),
    )


def _run(status="running"):
    return SimpleNamespace(status=status, project_id=uuid4())


class _FakeRedis:
    def __init__(self):
        self.enqueued = []

    async def enqueue_job(self, *args, **kwargs):
        self.enqueued.append((args, kwargs))

    async def aclose(self):
        pass


def _patch_redis(monkeypatch, redis):
    async def _get_redis_pool():
        return redis

    monkeypatch.setattr("src.workers.enqueue._get_redis_pool", _get_redis_pool)


def _patch_job(monkeypatch, result_info):
    class _FakeJob:
        def __init__(self, job_id, redis):
            pass

        async def result_info(self):
            return result_info

    monkeypatch.setattr("src.domain.codegen.provision_gate.Job", _FakeJob)


@pytest.mark.asyncio
async def test_no_prior_job_enqueues_and_returns_pending(monkeypatch):
    redis = _FakeRedis()
    _patch_redis(monkeypatch, redis)
    _patch_job(monkeypatch, None)

    result = await request_provisioning(_repo(_run()), uuid4(), {}, [], [])
    assert result == {"status": "pending"}
    assert len(redis.enqueued) == 1
    assert redis.enqueued[0][0][0] == "resolve_codegen_capabilities"


@pytest.mark.asyncio
async def test_successful_job_returns_ready(monkeypatch):
    _patch_redis(monkeypatch, _FakeRedis())
    _patch_job(monkeypatch, SimpleNamespace(success=True, result={"status": "ready"}))

    result = await request_provisioning(_repo(_run()), uuid4(), {}, [], [])
    assert result == {"status": "ready", "pack_install_outcomes": [], "composition_outcomes": []}


@pytest.mark.asyncio
async def test_ready_result_carries_optional_gap_outcomes_through(monkeypatch):
    """An optional gap doesn't block (that's "blocked", required-only) but must
    still reach the caller -- otherwise it silently never reaches the final run
    status/message for the GitHub Actions execution path."""
    _patch_redis(monkeypatch, _FakeRedis())
    outcomes = {
        "status": "ready",
        "pack_install_outcomes": [],
        "composition_outcomes": [{"name": "x", "status": "primitive_gap", "criticality": "optional"}],
    }
    _patch_job(monkeypatch, SimpleNamespace(success=True, result=outcomes))

    result = await request_provisioning(_repo(_run()), uuid4(), {}, [], [])
    assert result["composition_outcomes"] == outcomes["composition_outcomes"]


@pytest.mark.asyncio
async def test_primitive_gap_error_surfaces_as_blocked_not_failed(monkeypatch):
    _patch_redis(monkeypatch, _FakeRedis())
    gap = PrimitiveGapError("expense_report needs approval.quorum")
    _patch_job(monkeypatch, SimpleNamespace(success=False, result=gap))

    result = await request_provisioning(_repo(_run()), uuid4(), {}, [], [])
    assert result == {"status": "blocked", "message": "expense_report needs approval.quorum"}


@pytest.mark.asyncio
async def test_a_genuine_exception_still_surfaces_as_failed(monkeypatch):
    _patch_redis(monkeypatch, _FakeRedis())
    _patch_job(monkeypatch, SimpleNamespace(success=False, result=ValueError("Incompatible entity")))

    result = await request_provisioning(_repo(_run()), uuid4(), {}, [], [])
    assert result["status"] == "failed"
    assert result["failure_class"] == "invalid_requirement"
    assert result["retryable"] is False
    assert "Incompatible entity" in result["message"]


@pytest.mark.asyncio
async def test_pack_content_is_folded_into_the_dedup_fingerprint(monkeypatch):
    """Two different pack declarations for the same run must not collide onto the
    same idempotency key -- the fingerprint used to be provision-only."""
    redis_a = _FakeRedis()
    _patch_redis(monkeypatch, redis_a)
    _patch_job(monkeypatch, None)
    run_id = uuid4()

    await request_provisioning(_repo(_run()), run_id, {}, [{"pack_slug": "a"}], [])
    job_id_a = redis_a.enqueued[0][1]["_job_id"]

    redis_b = _FakeRedis()
    _patch_redis(monkeypatch, redis_b)
    await request_provisioning(_repo(_run()), run_id, {}, [{"pack_slug": "b"}], [])
    job_id_b = redis_b.enqueued[0][1]["_job_id"]

    assert job_id_a != job_id_b


# ---- classification: say why provisioning failed ---------------------------------


def _http_status_error(status):
    import httpx

    request = httpx.Request("GET", "https://cloud.test/v1/x")
    return httpx.HTTPStatusError(
        "boom", request=request, response=httpx.Response(status, request=request)
    )


def test_classification_of_each_failure_kind():
    import httpx
    from src.core.exceptions import ValidationError
    from src.domain.codegen.provision_gate import classify_provisioning_failure

    cases = [
        (ValueError("Entity x has incompatible access policy"), "invalid_requirement", False),
        (ValidationError("bad manifest"), "invalid_requirement", False),
        (_http_status_error(404), "cloud_rejected", False),
        (_http_status_error(503), "cloud_unavailable", True),
        (_http_status_error(429), "cloud_unavailable", True),
        (httpx.ConnectError("refused"), "cloud_unavailable", True),
        (AttributeError("'dict' object has no attribute 'function_slugs'"), "internal_error", True),
        (RuntimeError("anything else"), "internal_error", True),
        ("not even an exception", "internal_error", True),
    ]
    for error, failure_class, retryable in cases:
        got = classify_provisioning_failure(error)
        assert (got["failure_class"], got["retryable"]) == (failure_class, retryable), error


def test_an_internal_error_names_only_its_type_never_its_message():
    """An arbitrary exception string can hold internals; only our own validation
    messages are relayed to the runner, the run's error_message and the agent."""
    from src.domain.codegen.provision_gate import classify_provisioning_failure

    got = classify_provisioning_failure(RuntimeError("password=hunter2 at /home2/p2hs/.env"))
    assert "hunter2" not in got["message"]
    assert "/home2" not in got["message"]
    assert "RuntimeError" in got["message"]
    assert "not a problem with your request" in got["message"]


def test_schema_validation_errors_report_field_paths_and_are_bounded():
    from pydantic import BaseModel, Field, ValidationError
    from src.domain.codegen.provision_gate import classify_provisioning_failure

    class Model(BaseModel):
        name: str = Field(pattern=r"^[a-z]+$")

    try:
        Model.model_validate({"name": "Bad Name"})
    except ValidationError as exc:
        got = classify_provisioning_failure(exc)
    assert got["failure_class"] == "invalid_requirement"
    assert "name:" in got["message"]
    assert len(got["message"]) <= 500


@pytest.mark.asyncio
async def test_an_unexpected_job_crash_is_classified_internal_and_retryable(monkeypatch):
    """The production incident: the arq job died with AttributeError and the runner saw
    only "Required platform resource provisioning failed"."""
    _patch_redis(monkeypatch, _FakeRedis())
    crash = AttributeError("'dict' object has no attribute 'function_slugs'")
    _patch_job(monkeypatch, SimpleNamespace(success=False, result=crash))

    result = await request_provisioning(_repo(_run()), uuid4(), {}, [], [{"name": "x"}])
    assert result["status"] == "failed"
    assert result["failure_class"] == "internal_error"
    assert result["retryable"] is True
    assert "AttributeError" in result["message"]
    assert "function_slugs" not in result["message"]


@pytest.mark.asyncio
async def test_an_invalid_provision_manifest_fails_fast_instead_of_reading_as_pending(monkeypatch):
    """validate_manifest raising inside the HTTP handler used to look like "not resolved
    yet" to the runner, which retried for five minutes and then blamed connectivity."""
    _patch_redis(monkeypatch, _FakeRedis())
    _patch_job(monkeypatch, None)

    result = await request_provisioning(
        _repo(_run()), uuid4(), {"entities": [{"key": "Not Valid!"}]}, [], []
    )
    assert result["status"] == "failed"
    assert result["failure_class"] == "invalid_requirement"
    assert result["retryable"] is False


def test_the_runner_script_still_parses_and_reports_classified_failures():
    """The runner is JavaScript inside a Python string; a syntax slip there only shows up
    when a customer's next build runs."""
    import os
    import shutil
    import subprocess

    from src.domain.codegen.actions_pipeline import build_codegen_actions_runner

    script = build_codegen_actions_runner()
    assert 'result.status === "failed"' in script
    assert "result.message" in script
    node = shutil.which("node")
    if not node:
        pytest.skip("node is not installed")
    import tempfile

    with tempfile.TemporaryDirectory() as directory:
        path = os.path.join(directory, "runner.mjs")
        with open(path, "w", encoding="utf-8") as handle:
            handle.write(script)
        check = subprocess.run([node, "--check", path], capture_output=True, text=True)
    assert check.returncode == 0, check.stderr
