"""P1-3: product-catalog install verifies live catalog_item staff policy.

Does not upgrade historical absence. Conflicting/malformed policies are left
untouched. Lead historical upgrade is unchanged.
"""
import json
from copy import deepcopy
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch
from uuid import uuid4

import pytest
from src.domain.feature_packs.crm_authorization import (
    CRM_AUTHORIZATION_CONFLICT,
    CRM_AUTHORIZATION_UNVERIFIED,
    classify_policy,
    requires_crm_authorization,
    staff_policy,
)
from src.domain.feature_packs.service import PackInstallService

from tests.test_phase11_pack_migration_materialization import _captured_composition


def catalog_composition():
    return _captured_composition("alembic/versions/145_product_catalog_pack.py", "product-catalog")


class LiveCatalogCloud:
    def __init__(self, project, policy="absent", *, already_exists=True):
        schema = deepcopy(catalog_composition()["entities"][0]["schema"])
        schema.pop("access_policy", None)
        schema["fields"].append({"key": "custom", "type": "text", "indexed": True})
        schema["custom_metadata"] = {"retain": [1, 2]}
        if policy == "canonical":
            schema["access_policy"] = staff_policy()
        elif policy != "absent":
            schema["access_policy"] = policy
        self.already_exists = already_exists
        self.entity = {
            "project_id": str(project),
            "entity_key": "catalog_item",
            "status": "active",
            "version": 4,
            "schema_json": json.dumps(schema),
            "rls_enabled": 0,
            "owner_managed": 0,
            "owner_singleton": 0,
            "settings_json": '{"retain":true}',
        }
        self.patch_calls = []
        self.reads = 0

    async def create_project_entity(self, *args):
        return {"already_exists": True} if self.already_exists else {}

    async def get_project_entity(self, project, entity):
        self.reads += 1
        return {"entity": deepcopy(self.entity), "schema_update_precondition": "entity_version"}

    async def patch_project_entity(self, project, entity, body, *, expected_version=None):
        self.patch_calls.append(deepcopy(body))
        if expected_version is not None:
            assert expected_version == self.entity["version"]
        self.entity["schema_json"] = json.dumps(body["schema"])
        self.entity["version"] += 1

    async def get_project_schema(self, project):
        return {
            "entities": [
                {"key": "catalog_item", "fields": json.loads(self.entity["schema_json"])["fields"]}
            ]
        }


async def install_catalog(cloud, project, *, composition=None):
    pack = SimpleNamespace(
        id=uuid4(), slug="product-catalog", status="active",
        version="1.0.0", composition=composition or catalog_composition(), input_schema=None,
    )
    svc = PackInstallService(AsyncMock(), MagicMock(), cloud)
    svc._get_pack_by_slug = AsyncMock(return_value=pack)
    svc._installed_config = AsyncMock(return_value={})
    svc._upsert_install_state = AsyncMock()
    with patch(
        "src.domain.projects.cloud_schema_catalog.ProjectCloudSchemaSnapshotRepository.upsert",
        new_callable=AsyncMock,
    ):
        result = await svc.install_pack(project_id=project, pack_slug=pack.slug, config={})
    return result, svc._upsert_install_state


def test_product_catalog_requires_catalog_item_staff_policy():
    assert requires_crm_authorization("product-catalog", "1.0.0") == "catalog_item"
    assert requires_crm_authorization("lead-lifecycle", "1.1.0") == "lead"


def test_catalog_desired_composition_is_canonical():
    assert classify_policy(catalog_composition()["entities"][0]["schema"]) == "canonical_policy"


@pytest.mark.asyncio
async def test_fresh_catalog_install_verifies_live_staff_policy():
    project = uuid4()
    cloud = LiveCatalogCloud(project, "canonical", already_exists=False)
    result, state = await install_catalog(cloud, project)
    assert result["status"] == "installed"
    assert state.await_args.kwargs["status"] == "installed"
    assert json.loads(cloud.entity["schema_json"])["access_policy"] == staff_policy()
    assert cloud.patch_calls == []


@pytest.mark.asyncio
async def test_canonical_reinstall_is_idempotent():
    project = uuid4()
    cloud = LiveCatalogCloud(project, "canonical")
    result, _ = await install_catalog(cloud, project)
    assert result["ok"]
    result, _ = await install_catalog(cloud, project)
    assert result["ok"]
    assert cloud.patch_calls == []


@pytest.mark.asyncio
async def test_already_exists_without_policy_does_not_report_installed():
    project = uuid4()
    cloud = LiveCatalogCloud(project, "absent")
    before = deepcopy(cloud.entity)
    result, state = await install_catalog(cloud, project)
    assert result["status"] == "blocked"
    assert result["blocker"]["validation_code"] == CRM_AUTHORIZATION_UNVERIFIED
    assert result["blocker"]["reason"] == "installed policy absent"
    assert state.await_args.kwargs["status"] == "blocked"
    assert cloud.entity == before
    assert cloud.patch_calls == []


@pytest.mark.parametrize("policy", [
    {"version": 1, "roles": {"member": {"read": {"effect": "allow", "scope": "all"}}}},
    None,
    {},
    {"version": 1, "roles": {}},
])
@pytest.mark.asyncio
async def test_custom_or_malformed_catalog_policy_is_not_overwritten(policy):
    project = uuid4()
    cloud = LiveCatalogCloud(project, policy)
    before = deepcopy(cloud.entity)
    for _ in range(2):
        result, state = await install_catalog(cloud, project)
        assert not result["ok"]
        assert result["blocker"]["validation_code"] == CRM_AUTHORIZATION_CONFLICT
        assert state.await_args.kwargs["status"] == "blocked"
    assert cloud.entity == before
    assert cloud.patch_calls == []
