"""Chaos — Job polling: timeout, failure, cancel, flapping status."""

from __future__ import annotations

from unittest.mock import MagicMock, patch

import httpx
import pytest
import respx

from kanoniv.cloud import reconcile

from .conftest import (
    API_KEY,
    BASE_URL,
    JOB_ID,
    make_completed_job_response,
    make_job_run_response,
    make_mock_source,
    make_mock_spec,
)


def _setup_base_routes(router: respx.Router) -> None:
    """Wire spec ingest and entity ingest routes."""
    router.post("/v1/identity/specs").mock(
        return_value=httpx.Response(200, json={"valid": True}),
    )
    router.post(url__regex=r"/v1/ingest/webhook/.+").mock(
        return_value=httpx.Response(200, json={"ingested": 1}),
    )


# ---------------------------------------------------------------------------
# Timeout
# ---------------------------------------------------------------------------

class TestPollingTimeout:
    """TimeoutError when job doesn't complete within deadline."""

    @patch("kanoniv.cloud.time.sleep")
    @patch("kanoniv.cloud.time.monotonic")
    def test_timeout_error_raised(
        self, mock_monotonic: MagicMock, mock_sleep: MagicMock,
        mock_api: respx.Router,
    ):
        _setup_base_routes(mock_api)
        mock_api.post("/v1/jobs/run").mock(
            return_value=httpx.Response(200, json=make_job_run_response(status="pending")),
        )
        mock_api.get(f"/v1/jobs/{JOB_ID}").mock(
            return_value=httpx.Response(200, json={"id": JOB_ID, "status": "running"}),
        )
        # monotonic: first call sets deadline, subsequent calls exceed it
        mock_monotonic.side_effect = [0.0, 301.0]

        with pytest.raises(TimeoutError):
            reconcile(
                [make_mock_source()], make_mock_spec(),
                api_key=API_KEY, base_url=BASE_URL, timeout=300.0,
            )

    @patch("kanoniv.cloud.time.sleep")
    @patch("kanoniv.cloud.time.monotonic")
    def test_timeout_message_includes_job_id(
        self, mock_monotonic: MagicMock, mock_sleep: MagicMock,
        mock_api: respx.Router,
    ):
        _setup_base_routes(mock_api)
        mock_api.post("/v1/jobs/run").mock(
            return_value=httpx.Response(200, json=make_job_run_response(status="pending")),
        )
        mock_api.get(f"/v1/jobs/{JOB_ID}").mock(
            return_value=httpx.Response(200, json={"id": JOB_ID, "status": "running"}),
        )
        mock_monotonic.side_effect = [0.0, 301.0]

        with pytest.raises(TimeoutError, match=JOB_ID):
            reconcile(
                [make_mock_source()], make_mock_spec(),
                api_key=API_KEY, base_url=BASE_URL, timeout=300.0,
            )

    @patch("kanoniv.cloud.time.sleep")
    @patch("kanoniv.cloud.time.monotonic")
    def test_custom_timeout_respected(
        self, mock_monotonic: MagicMock, mock_sleep: MagicMock,
        mock_api: respx.Router,
    ):
        _setup_base_routes(mock_api)
        mock_api.post("/v1/jobs/run").mock(
            return_value=httpx.Response(200, json=make_job_run_response(status="pending")),
        )
        mock_api.get(f"/v1/jobs/{JOB_ID}").mock(
            return_value=httpx.Response(200, json={"id": JOB_ID, "status": "running"}),
        )
        mock_monotonic.side_effect = [0.0, 11.0]

        with pytest.raises(TimeoutError, match=r"10\.0s"):
            reconcile(
                [make_mock_source()], make_mock_spec(),
                api_key=API_KEY, base_url=BASE_URL, timeout=10.0,
            )


# ---------------------------------------------------------------------------
# Failure
# ---------------------------------------------------------------------------

class TestPollingFailure:
    """Failed and cancelled jobs raise RuntimeError."""

    @patch("kanoniv.cloud.time.sleep")
    def test_failed_raises_runtime_error(
        self, mock_sleep: MagicMock, mock_api: respx.Router,
    ):
        _setup_base_routes(mock_api)
        mock_api.post("/v1/jobs/run").mock(
            return_value=httpx.Response(200, json=make_job_run_response(status="pending")),
        )
        mock_api.get(f"/v1/jobs/{JOB_ID}").mock(
            return_value=httpx.Response(
                200, json={"id": JOB_ID, "status": "failed", "error": "out of memory"},
            ),
        )

        with pytest.raises(RuntimeError, match="out of memory"):
            reconcile(
                [make_mock_source()], make_mock_spec(),
                api_key=API_KEY, base_url=BASE_URL,
            )

    @patch("kanoniv.cloud.time.sleep")
    def test_failed_error_message_propagated(
        self, mock_sleep: MagicMock, mock_api: respx.Router,
    ):
        _setup_base_routes(mock_api)
        mock_api.post("/v1/jobs/run").mock(
            return_value=httpx.Response(200, json=make_job_run_response(status="pending")),
        )
        mock_api.get(f"/v1/jobs/{JOB_ID}").mock(
            return_value=httpx.Response(
                200, json={"id": JOB_ID, "status": "failed", "error": "schema mismatch"},
            ),
        )

        with pytest.raises(RuntimeError, match=JOB_ID):
            reconcile(
                [make_mock_source()], make_mock_spec(),
                api_key=API_KEY, base_url=BASE_URL,
            )

    @patch("kanoniv.cloud.time.sleep")
    def test_cancelled_raises_runtime_error(
        self, mock_sleep: MagicMock, mock_api: respx.Router,
    ):
        _setup_base_routes(mock_api)
        mock_api.post("/v1/jobs/run").mock(
            return_value=httpx.Response(200, json=make_job_run_response(status="pending")),
        )
        mock_api.get(f"/v1/jobs/{JOB_ID}").mock(
            return_value=httpx.Response(
                200, json={"id": JOB_ID, "status": "cancelled"},
            ),
        )

        with pytest.raises(RuntimeError, match="cancelled"):
            reconcile(
                [make_mock_source()], make_mock_spec(),
                api_key=API_KEY, base_url=BASE_URL,
            )


# ---------------------------------------------------------------------------
# Edge cases
# ---------------------------------------------------------------------------

class TestPollingEdgeCases:
    """Status flapping, job_id extraction variants."""

    @patch("kanoniv.cloud.time.sleep")
    def test_status_flapping_still_completes(
        self, mock_sleep: MagicMock, mock_api: respx.Router,
    ):
        _setup_base_routes(mock_api)
        mock_api.post("/v1/jobs/run").mock(
            return_value=httpx.Response(200, json=make_job_run_response(status="pending")),
        )
        mock_api.get(f"/v1/jobs/{JOB_ID}").mock(
            side_effect=[
                httpx.Response(200, json={"id": JOB_ID, "status": "running"}),
                httpx.Response(200, json={"id": JOB_ID, "status": "pending"}),
                httpx.Response(200, json={"id": JOB_ID, "status": "running"}),
                httpx.Response(200, json=make_completed_job_response()),
            ],
        )

        result = reconcile(
            [make_mock_source()], make_mock_spec(),
            api_key=API_KEY, base_url=BASE_URL,
        )
        assert result.status == "completed"

    @patch("kanoniv.cloud.time.sleep")
    def test_job_id_from_id_field(
        self, mock_sleep: MagicMock, mock_api: respx.Router,
    ):
        _setup_base_routes(mock_api)
        mock_api.post("/v1/jobs/run").mock(
            return_value=httpx.Response(
                200, json={"id": "j-from-id", "status": "pending"},
            ),
        )
        completed = make_completed_job_response("j-from-id")
        mock_api.get("/v1/jobs/j-from-id").mock(
            return_value=httpx.Response(200, json=completed),
        )

        result = reconcile(
            [make_mock_source()], make_mock_spec(),
            api_key=API_KEY, base_url=BASE_URL,
        )
        assert result.job_id == "j-from-id"

    @patch("kanoniv.cloud.time.sleep")
    def test_job_id_fallback_to_job_id_field(
        self, mock_sleep: MagicMock, mock_api: respx.Router,
    ):
        _setup_base_routes(mock_api)
        mock_api.post("/v1/jobs/run").mock(
            return_value=httpx.Response(
                200, json={"job_id": "j-fallback", "status": "pending"},
            ),
        )
        completed = make_completed_job_response("j-fallback")
        mock_api.get("/v1/jobs/j-fallback").mock(
            return_value=httpx.Response(200, json=completed),
        )

        result = reconcile(
            [make_mock_source()], make_mock_spec(),
            api_key=API_KEY, base_url=BASE_URL,
        )
        assert result.job_id == "j-fallback"
