import json
from copy import deepcopy
from unittest.mock import AsyncMock, mock_open

import pytest
from pydantic import ValidationError
from src.domain.codegen.entity_catalog import resolve_database_view
from src.domain.codegen.provision_manifest import (
    load_manifest_from_repo_path,
    provision_entities,
    validate_manifest,
)
from src.domain.manifest.schemas import (
    ManifestAuthSection,
    ManifestBackendSection,
    ManifestDatabaseSection,
    ManifestDataImportSection,
    ManifestQueryDslSection,
)
from src.integrations.cloud_schema import normalize_cloud_entity


def entity():
    return validate_manifest(
        {
            "entities": [
                {
                    "key": "contacts",
                    "label": "Contacts",
                    "fields": [{"key": "name", "label": "Name", "type": "text"}],
                }
            ]
        }
    )["entities"][0]


def test_typed_policy_and_unavailable_are_not_empty():
    assert ManifestAuthSection(enabled=True).user_directory == "platform_only"
    assert ManifestDatabaseSection().persistence == "cloud_record_engine"
    assert ManifestDatabaseSection().entities is None
    assert ManifestDatabaseSection(schema_status="omitted").entities is None
    backend = ManifestBackendSection()
    assert backend.provider == "p2hs_cloud"
    assert backend.generated_app == "ui_and_bff"
    assert backend.customer_backend is None
    assert backend.uses == "p2hs_cloud"
    assert backend.policy_authority == "p2hs_core"
    query_dsl = ManifestQueryDslSection()
    assert query_dsl.d1_ceiling["use_external_connections_when"] == []
    assert "Do not create a local database" in query_dsl.d1_ceiling["hard_ceiling_note"]
    assert "Cloud record engine" in query_dsl.guidance
    assert ManifestDataImportSection().enabled is False
    assert resolve_database_view(live_status="ok", live_entities=[], cached_entities=[]) == {
        "schema_status": "empty",
        "schema_source": "live",
        "entities": [],
        "persist": [],
    }


def test_failed_fetch_retains_known_keys_without_claiming_ready():
    result = resolve_database_view(
        live_status="error", live_entities=[], cached_entities=[entity()]
    )
    assert result["schema_status"] == "unavailable"
    assert result["entities"][0]["key"] == "contacts"
    assert result["persist"] is None


def test_normalize_actual_cloud_schema_preserves_metadata():
    schema = {"fields": [{"key": "name", "type": "text", "indexed": True}], "version": 4}
    result = normalize_cloud_entity({"entity_key": "contacts", "schema_json": json.dumps(schema)})
    assert result["key"] == "contacts"
    assert result["schema"] == schema
    assert result["fields"][0]["indexed"] is True


@pytest.mark.parametrize("schema", [None, {}, {"fields": [None]}, {"fields": [{"key": "name"}]}])
def test_malformed_schema_never_becomes_zero_fields(schema):
    with pytest.raises(ValueError):
        normalize_cloud_entity({"entity_key": "contacts", "schema_json": schema})


@pytest.mark.parametrize(
    "body",
    [
        {"entities": [entity(), entity()]},
        {"entities": [{**entity(), "drop": True}]},
        {"storage_buckets": [{"name": "uploads", "rls_enabled": "false"}]},
        {"entities": [{**entity(), "fields": [entity()["fields"][0]] * 2}]},
    ],
)
def test_invalid_resource_declarations_fail_closed(body):
    with pytest.raises(ValidationError):
        validate_manifest(body)


def test_invalid_file_is_not_treated_as_absent(monkeypatch):
    monkeypatch.setattr("os.path.isfile", lambda _: True)
    monkeypatch.setattr("builtins.open", mock_open(read_data="{"))
    with pytest.raises(json.JSONDecodeError):
        load_manifest_from_repo_path("checkout")


@pytest.mark.asyncio
async def test_retry_of_compatible_entity_does_not_write():
    client = AsyncMock()
    client.get_project_schema.return_value = {"entities": [entity()]}
    result = await provision_entities(client, "project", [entity()])
    assert result[0]["action"] == "existing"
    client.create_project_entity.assert_not_awaited()
    client.patch_project_entity.assert_not_awaited()


@pytest.mark.asyncio
async def test_409_does_not_hide_incompatible_schema():
    client = AsyncMock()
    bad = deepcopy(entity())
    bad["fields"][0]["type"] = "number"
    client.get_project_schema.side_effect = [{"entities": []}, {"entities": [bad]}]
    client.create_project_entity.return_value = {"already_exists": True}
    with pytest.raises(ValueError, match="Incompatible"):
        await provision_entities(client, "project", [entity()])


@pytest.mark.asyncio
async def test_missing_known_entity_never_recreated_empty():
    client = AsyncMock()
    client.get_project_schema.return_value = {"entities": []}
    with pytest.raises(ValueError, match="Previously known"):
        await provision_entities(client, "project", [entity()], known_entities=[entity()])
    client.create_project_entity.assert_not_awaited()


@pytest.mark.asyncio
async def test_outage_never_mutates_schema():
    client = AsyncMock()
    client.get_project_schema.side_effect = RuntimeError("offline")
    with pytest.raises(ValueError, match="unavailable"):
        await provision_entities(client, "project", [entity()])
    client.create_project_entity.assert_not_awaited()
    client.patch_project_entity.assert_not_awaited()


@pytest.mark.asyncio
async def test_additive_field_preserves_live_metadata():
    client = AsyncMock()
    live = entity()
    live["fields"][0]["indexed"] = True
    live["schema"] = {"fields": live["fields"], "version": 7}
    declared = entity()
    declared["fields"].append({"key": "age", "label": "Age", "type": "number"})
    accepted = deepcopy(live)
    accepted["fields"].append(declared["fields"][-1])
    client.get_project_schema.side_effect = [{"entities": [live]}, {"entities": [accepted]}]
    await provision_entities(client, "project", [declared])
    patched = client.patch_project_entity.await_args.args[2]["schema"]
    assert patched["version"] == 7
    assert patched["fields"][0]["indexed"] is True
    assert len(patched["fields"]) == 2
