from src.workers.tasks.reconcile_project_capabilities import (
    _project_feature_pack_contract_query,
)
from src.domain.feature_packs.reconciliation import (
    build_contract_snapshot,
    contract_fingerprint,
    reconciliation_status,
)


def test_project_feature_pack_query_exposes_contract_slug_key():
    assert "slug" in _project_feature_pack_contract_query().selected_columns.keys()
    assert "pack_slug" not in _project_feature_pack_contract_query().selected_columns.keys()


def test_contract_snapshot_is_stable_and_order_independent():
    left = build_contract_snapshot([
        {"slug": "messaging", "installed_version": "1.0.0", "current_version": "1.1.0", "status": "installed"},
        {"slug": "case-lifecycle", "installed_version": "1.0.0", "current_version": "1.0.0", "status": "deployed"},
    ])
    right = build_contract_snapshot(list(reversed([
        {"slug": "messaging", "installed_version": "1.0.0", "current_version": "1.1.0", "status": "installed"},
        {"slug": "case-lifecycle", "installed_version": "1.0.0", "current_version": "1.0.0", "status": "deployed"},
    ])))
    assert left == right
    assert contract_fingerprint(left) == contract_fingerprint(right)


def test_reconciliation_separates_policy_apply_from_code_upgrade():
    assert reconciliation_status({"schema_version": 1, "packs": [
        {"installed_version": "1.0.0", "current_version": "1.1.0"},
    ]}) == ("upgrade_available", "regenerate_or_reinstall_pack")
    assert reconciliation_status({"schema_version": 1, "packs": [
        {"installed_version": "1.1.0", "current_version": "1.1.0"},
    ]}) == ("applied", None)


# ---- policy refresh: unbuilt packs are skipped, missing entities are drift -------------------

from types import SimpleNamespace  # noqa: E402
from unittest.mock import AsyncMock  # noqa: E402

import httpx  # noqa: E402
import pytest  # noqa: E402
from src.workers.tasks.reconcile_project_capabilities import (  # noqa: E402
    _refresh_entity_policies,
    drift_summary,
)


def _pack(slug, status, *entities):
    installed = SimpleNamespace(pack_slug=slug, status=status)
    definition = SimpleNamespace(composition={"entities": [{"entity_key": e} for e in entities]})
    return installed, definition


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


@pytest.mark.asyncio
@pytest.mark.parametrize("status", ["blocked", "uninstalled", "supported", "enabled"])
async def test_a_pack_that_never_built_its_entities_is_not_asked_about_them(status):
    """Production: contact-capture was `blocked`, so contact_submission never existed, and the
    cron 404ed on it every cycle with a full stack trace."""
    cloud = SimpleNamespace(reconcile_project_entity_policy=AsyncMock())
    missing = await _refresh_entity_policies(
        cloud, "p1", [_pack("contact-capture", status, "contact_submission")]
    )
    assert missing == []
    cloud.reconcile_project_entity_policy.assert_not_awaited()


@pytest.mark.asyncio
@pytest.mark.parametrize(
    "status", ["installed", "configured", "deployed", "verified", "configuration_required"]
)
async def test_every_state_that_provisioned_entities_is_refreshed(status):
    cloud = SimpleNamespace(reconcile_project_entity_policy=AsyncMock())
    missing = await _refresh_entity_policies(cloud, "p1", [_pack("crm", status, "lead", "deal")])
    assert missing == []
    assert cloud.reconcile_project_entity_policy.await_count == 2


@pytest.mark.asyncio
async def test_an_installed_pack_whose_entity_is_gone_is_drift_not_an_error_and_others_continue():
    async def refresh(project_id, entity_def):
        if entity_def["entity_key"] == "contact_submission":
            raise _http_error(404)

    cloud = SimpleNamespace(reconcile_project_entity_policy=AsyncMock(side_effect=refresh))
    packs = [
        _pack("contact-capture", "installed", "contact_submission"),
        _pack("crm", "installed", "lead"),
    ]
    missing = await _refresh_entity_policies(cloud, "p1", packs)
    assert missing == ["contact-capture:contact_submission"]
    assert cloud.reconcile_project_entity_policy.await_count == 2  # `lead` was still refreshed


@pytest.mark.asyncio
@pytest.mark.parametrize("code", [500, 502, 403, 401])
async def test_any_other_cloud_failure_still_raises(code):
    cloud = SimpleNamespace(
        reconcile_project_entity_policy=AsyncMock(side_effect=_http_error(code))
    )
    with pytest.raises(httpx.HTTPStatusError):
        await _refresh_entity_policies(cloud, "p1", [_pack("crm", "installed", "lead")])


def test_drift_is_recorded_with_a_required_action():
    status, action, message = drift_summary(["b:x", "a:y", "a:y"])
    assert (status, action) == ("drift", "reinstall_pack")
    assert message == "Installed pack entities are missing in Cloud: a:y, b:x"
