from types import SimpleNamespace
from uuid import uuid4

import pytest
import src.domain.codegen.app_block_plan as app_block_plan
from src.domain.app_blocks.adoption import adoption_for, apply_adoption, install_and_protect_packs
from src.domain.app_blocks.compiler import compile_install_plan
from src.domain.app_blocks.pack_install import merge_pack_install_entries
from src.domain.projects.discovery_schemas import (
    Actor,
    AuthenticationSpec,
    DiscoverySpec,
    Entity,
    ProductDefinition,
    TenancySpec,
    Workflow,
)


def test_merge_does_not_invent_contact_capture():
    merged = merge_pack_install_entries(
        [{"pack_slug": "appointment-booking", "config": {"x": 1}}],
        install_slugs=[],
    )
    assert [row["pack_slug"] for row in merged] == ["appointment-booking"]
    assert merged[0]["config"] == {"x": 1}


def test_merge_adds_plan_required_packs():
    merged = merge_pack_install_entries([], install_slugs=["contact-capture"])
    assert merged == [{"pack_slug": "contact-capture", "config": {}}]


def test_authoritative_installs_and_protects_contact_pack():
    spec = DiscoverySpec(
        product=ProductDefinition(
            purpose="Allow prospective customers to contact the agency.",
            first_build_goal="Capture website enquiries.",
        ),
        actors=[Actor(id="visitor", name="Website visitor")],
        workflows=[
            Workflow(id="submit", name="Submit enquiry", description="Visitor sends a message")
        ],
        entities=[Entity(id="enquiry", name="Enquiry", description="A website enquiry")],
        tenancy=TenancySpec(model="single_workspace"),
        authentication=AuthenticationSpec(required=False),
    )
    plan = apply_adoption(compile_install_plan(spec), "authoritative")
    install, protect = install_and_protect_packs(plan)
    assert "contact-capture" in install
    assert "contact-capture" in protect


def test_advisory_protects_without_forcing_install():
    spec = DiscoverySpec(
        product=ProductDefinition(
            purpose="Allow prospective customers to contact the agency.",
            first_build_goal="Capture website enquiries.",
        ),
        actors=[Actor(id="visitor", name="Website visitor")],
        workflows=[
            Workflow(id="submit", name="Submit enquiry", description="Visitor sends a message")
        ],
        entities=[Entity(id="enquiry", name="Enquiry", description="A website enquiry")],
        tenancy=TenancySpec(model="single_workspace"),
        authentication=AuthenticationSpec(required=False),
    )
    plan = apply_adoption(compile_install_plan(spec), "advisory")
    install, protect = install_and_protect_packs(plan)
    assert install == []
    assert "contact-capture" in protect


def test_imported_projects_omit_plan_packs():
    assert adoption_for(source_kind="github_import", completed_codegen=False) == "omitted"
    assert adoption_for(source_kind="generated", completed_codegen=False) == "authoritative"
    assert adoption_for(source_kind="generated", completed_codegen=True) == "advisory"


class _Rows:
    def __init__(self, rows):
        self._rows = rows

    def all(self):
        return self._rows


class _ReconciliationDb:
    def __init__(self, generation_plan, installed=()):
        self.generation_plan = generation_plan
        self.installed = installed

    async def scalar(self, _query):
        return 1

    async def execute(self, query):
        text = str(query)
        if "project_feature_packs" in text:
            return _Rows(self.installed)
        return _Rows([({"generation_plan": self.generation_plan},)])


@pytest.mark.asyncio
async def test_advisory_reconciliation_repairs_missing_pack_from_generation_plan(monkeypatch):
    project_id = uuid4()
    generation_plan = {
        "composition": {
            "app_block_ids": ["forms.quote_request"],
            "feature_pack_ids": ["contact-capture"],
        },
        "runtime": {},
    }

    class _Discovery:
        async def get(self, _project_id):
            return SimpleNamespace(source_kind="generated", discovery_json={})

    monkeypatch.setattr(app_block_plan, "DiscoveryRepository", lambda _db: _Discovery())
    policy = await app_block_plan.resolve_pack_policy(
        _ReconciliationDb(generation_plan), project_id
    )

    assert policy == ("advisory", ["contact-capture"], ["contact-capture"])


@pytest.mark.asyncio
async def test_advisory_reconciliation_preserves_healthy_pack(monkeypatch):
    project_id = uuid4()
    generation_plan = {
        "composition": {
            "app_block_ids": ["forms.quote_request"],
            "feature_pack_ids": ["contact-capture"],
        },
        "runtime": {},
    }

    class _Discovery:
        async def get(self, _project_id):
            return SimpleNamespace(source_kind="generated", discovery_json={})

    monkeypatch.setattr(app_block_plan, "DiscoveryRepository", lambda _db: _Discovery())
    policy = await app_block_plan.resolve_pack_policy(
        _ReconciliationDb(
            generation_plan,
            installed=[("contact-capture", "installed")],
        ),
        project_id,
    )

    assert policy == ("advisory", [], ["contact-capture"])


@pytest.mark.asyncio
async def test_second_reconciliation_after_pack_install_is_noop(monkeypatch):
    """A scheduled reconciliation must not reinstall an already healthy pack."""
    project_id = uuid4()
    generation_plan = {
        "composition": {
            "app_block_ids": ["forms.quote_request"],
            "feature_pack_ids": ["contact-capture"],
        },
        "runtime": {},
    }

    class _Discovery:
        async def get(self, _project_id):
            return SimpleNamespace(source_kind="generated", discovery_json={})

    monkeypatch.setattr(app_block_plan, "DiscoveryRepository", lambda _db: _Discovery())
    first = await app_block_plan.resolve_pack_policy(_ReconciliationDb(generation_plan), project_id)
    second = await app_block_plan.resolve_pack_policy(
        _ReconciliationDb(generation_plan, installed=[("contact-capture", "installed")]),
        project_id,
    )

    assert first[1] == ["contact-capture"]
    assert second[1] == []
