"""Tests for Phase 6 — module system: manifest registry, lifecycle, flags, API."""

from __future__ import annotations

import pytest
from django.urls import reverse

from simorgh.apps.events.bus import clear_subscribers, subscribe
from simorgh.apps.modules.models import FeatureFlag, Module, ModuleStatus
from simorgh.apps.modules.registry import (
    FeatureFlagSpec,
    ModuleError,
    ModuleManifest,
    ServiceContract,
    deserialize,
    get_module,
    list_modules,
    register_module,
    reset_registry_for_tests,
    resolve_install_order,
)
from simorgh.apps.modules.services import (
    disable_module,
    enable_module,
    install_module,
    is_feature_enabled,
    rollback_module,
    set_feature_flag,
    upgrade_module,
)


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture
def _module_registry():
    reset_registry_for_tests()
    clear_subscribers()
    yield
    reset_registry_for_tests()
    clear_subscribers()


def _make_manifest(name, *, version=1, depends=(), flags=()):
    return ModuleManifest(
        name=name,
        version=version,
        label_key=f"module.{name}.label",
        depends_on=tuple(depends),
        feature_flags=tuple(flags),
    )


@pytest.fixture
def crm_stack(_module_registry):
    acct = _make_manifest("accounting")
    crm = _make_manifest(
        "crm",
        depends=("accounting",),
        flags=(
            FeatureFlagSpec(name="lead_scoring", label_key="crm.flag.scoring"),
            FeatureFlagSpec(
                name="ai_summary", label_key="crm.flag.ai", scope="user", default=False,
            ),
        ),
    )
    helpdesk = _make_manifest("helpdesk", depends=("crm",))
    register_module(acct)
    register_module(crm)
    register_module(helpdesk)
    return {"accounting": acct, "crm": crm, "helpdesk": helpdesk}


# ---------------------------------------------------------------------------
# Registry & manifests
# ---------------------------------------------------------------------------
def test_manifest_validation_bad_name(_module_registry):
    with pytest.raises(ModuleError):
        _make_manifest("Bad.Name")


def test_manifest_self_dependency(_module_registry):
    with pytest.raises(ModuleError):
        ModuleManifest(name="crm", version=1, label_key="x", depends_on=("crm",))


def test_manifest_duplicate_flag(_module_registry):
    flag = FeatureFlagSpec(name="x", label_key="y")
    with pytest.raises(ModuleError):
        ModuleManifest(
            name="crm", version=1, label_key="x", feature_flags=(flag, flag),
        )


def test_register_module_idempotent(_module_registry):
    m = _make_manifest("crm")
    assert register_module(m) is m
    assert register_module(m) is m
    other = _make_manifest("crm", version=2)
    with pytest.raises(ModuleError):
        register_module(other)


def test_serialize_round_trip(_module_registry):
    m = _make_manifest(
        "crm",
        flags=(FeatureFlagSpec(name="x", label_key="l"),),
    )
    payload = m.serialize()
    assert deserialize(payload) == m


def test_service_contract_validation(_module_registry):
    with pytest.raises(ModuleError):
        ServiceContract(name="crm.api", provider="no_colon")
    c = ServiceContract(name="crm.api", provider="simorgh.apps.modules.registry:register_module")
    assert c.load() is register_module


def test_get_and_list_modules(crm_stack):
    assert get_module("crm").name == "crm"
    names = [m.name for m in list_modules()]
    assert names == ["accounting", "crm", "helpdesk"]


# ---------------------------------------------------------------------------
# Dependency resolver
# ---------------------------------------------------------------------------
def test_resolve_install_order_topological(crm_stack):
    order = [m.name for m in resolve_install_order(["helpdesk"])]
    assert order == ["accounting", "crm", "helpdesk"]


def test_resolve_install_order_unknown(crm_stack):
    with pytest.raises(ModuleError):
        resolve_install_order(["nope"])


def test_resolve_cycle_detected(_module_registry):
    # Build a cycle by registering manifests whose deps reference each other.
    a = _make_manifest("a", depends=("b",))
    b = _make_manifest("b", depends=("a",))
    register_module(a)
    register_module(b)
    with pytest.raises(ModuleError):
        resolve_install_order(["a"])


def test_resolve_unregistered_dependency(_module_registry):
    register_module(_make_manifest("crm", depends=("accounting",)))
    with pytest.raises(ModuleError):
        resolve_install_order(["crm"])


# ---------------------------------------------------------------------------
# Install / enable / disable
# ---------------------------------------------------------------------------
@pytest.mark.django_db
def test_install_pulls_dependencies(crm_stack, tenant_acme, acme_tree):
    rows = install_module(tenant_acme, "helpdesk")
    names = [r.name for r in rows]
    assert names == ["accounting", "crm", "helpdesk"]
    assert Module.objects.filter(tenant=tenant_acme).count() == 3
    assert all(r.status == ModuleStatus.INSTALLED for r in rows)


@pytest.mark.django_db
def test_install_is_idempotent(crm_stack, tenant_acme, acme_tree):
    first = install_module(tenant_acme, "crm")
    again = install_module(tenant_acme, "crm")
    assert [r.pk for r in first] == [r.pk for r in again]


@pytest.mark.django_db
def test_install_without_tenant_root(crm_stack, tenant_acme):
    # No org tree fixture used here.
    with pytest.raises(ModuleError):
        install_module(tenant_acme, "accounting")


@pytest.mark.django_db
def test_enable_requires_dependency_enabled(crm_stack, tenant_acme, acme_tree):
    install_module(tenant_acme, "crm")
    with pytest.raises(ModuleError):
        enable_module(tenant_acme, "crm")
    enable_module(tenant_acme, "accounting")
    row = enable_module(tenant_acme, "crm")
    assert row.status == ModuleStatus.ENABLED


@pytest.mark.django_db
def test_disable_blocked_when_dependent_enabled(crm_stack, tenant_acme, acme_tree):
    install_module(tenant_acme, "crm")
    enable_module(tenant_acme, "accounting")
    enable_module(tenant_acme, "crm")
    with pytest.raises(ModuleError):
        disable_module(tenant_acme, "accounting")
    disable_module(tenant_acme, "crm")
    row = disable_module(tenant_acme, "accounting")
    assert row.status == ModuleStatus.DISABLED


# ---------------------------------------------------------------------------
# Upgrade / rollback
# ---------------------------------------------------------------------------
@pytest.mark.django_db
def test_upgrade_and_rollback(_module_registry, tenant_acme, acme_tree):
    register_module(_make_manifest("crm", version=1))
    install_module(tenant_acme, "crm")
    reset_registry_for_tests()
    register_module(_make_manifest("crm", version=3))
    upgraded = upgrade_module(tenant_acme, "crm")
    assert upgraded.version == 3
    assert upgraded.previous_version == 1
    rolled = rollback_module(tenant_acme, "crm")
    assert rolled.version == 1
    assert rolled.previous_version is None


@pytest.mark.django_db
def test_upgrade_noop_when_versions_match(crm_stack, tenant_acme, acme_tree):
    install_module(tenant_acme, "accounting")
    row = upgrade_module(tenant_acme, "accounting")
    assert row.version == 1
    assert row.previous_version is None


@pytest.mark.django_db
def test_rollback_without_previous_fails(crm_stack, tenant_acme, acme_tree):
    install_module(tenant_acme, "accounting")
    with pytest.raises(ModuleError):
        rollback_module(tenant_acme, "accounting")


# ---------------------------------------------------------------------------
# Feature flags
# ---------------------------------------------------------------------------
@pytest.mark.django_db
def test_feature_flag_default_when_disabled(crm_stack, tenant_acme, acme_tree):
    install_module(tenant_acme, "crm")
    # crm is installed but not enabled → resolution returns False unconditionally.
    assert is_feature_enabled(tenant_acme, "crm", "lead_scoring") is False


@pytest.mark.django_db
def test_feature_flag_precedence(crm_stack, tenant_acme, acme_tree, alice):
    install_module(tenant_acme, "crm")
    enable_module(tenant_acme, "accounting")
    enable_module(tenant_acme, "crm")
    # Default is False.
    assert is_feature_enabled(tenant_acme, "crm", "ai_summary", user_id=alice.pk) is False
    # Tenant override → True.
    set_feature_flag(tenant_acme, "crm", "ai_summary", enabled=True)
    assert is_feature_enabled(tenant_acme, "crm", "ai_summary", user_id=alice.pk) is True
    # User override → False wins.
    set_feature_flag(tenant_acme, "crm", "ai_summary", enabled=False, user_id=alice.pk)
    assert is_feature_enabled(tenant_acme, "crm", "ai_summary", user_id=alice.pk) is False
    # But another user still gets the tenant value.
    assert is_feature_enabled(tenant_acme, "crm", "ai_summary", user_id=alice.pk + 999) is True


@pytest.mark.django_db
def test_tenant_scope_cannot_be_set_per_user(crm_stack, tenant_acme, acme_tree, alice):
    install_module(tenant_acme, "crm")
    with pytest.raises(ModuleError):
        set_feature_flag(
            tenant_acme, "crm", "lead_scoring", enabled=True, user_id=alice.pk,
        )


@pytest.mark.django_db
def test_feature_flag_dispatches_event(crm_stack, tenant_acme, acme_tree):
    received: list[dict] = []

    @subscribe("modules.feature_flag_changed")
    def _h(payload):
        received.append(payload)

    install_module(tenant_acme, "crm")
    set_feature_flag(tenant_acme, "crm", "lead_scoring", enabled=True)
    assert received and received[-1]["flag"] == "lead_scoring"


# ---------------------------------------------------------------------------
# HTTP API
# ---------------------------------------------------------------------------
@pytest.mark.django_db
def test_module_list_requires_auth(api_client, crm_stack):
    resp = api_client.get(reverse("platform_modules:module-list"))
    assert resp.status_code in (401, 403)


@pytest.mark.django_db
def test_module_list_returns_manifests(
    api_client, crm_stack, tenant_acme, acme_tree, alice,
):
    alice.is_superuser = True
    alice.save()
    api_client.force_login(alice)
    resp = api_client.get(
        reverse("platform_modules:module-list"),
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert resp.status_code == 200
    names = [r["manifest"]["name"] for r in resp.json()["results"]]
    assert names == ["accounting", "crm", "helpdesk"]


@pytest.mark.django_db
def test_module_install_endpoint(
    api_client, crm_stack, tenant_acme, acme_tree, alice,
):
    alice.is_superuser = True
    alice.save()
    api_client.force_login(alice)
    resp = api_client.post(
        reverse("platform_modules:module-install", args=["crm"]),
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert resp.status_code == 201
    assert resp.json()["installed"] == ["accounting", "crm"]
    assert Module.objects.filter(tenant=tenant_acme).count() == 2


@pytest.mark.django_db
def test_lifecycle_via_api(
    api_client, crm_stack, tenant_acme, acme_tree, alice,
):
    alice.is_superuser = True
    alice.save()
    api_client.force_login(alice)
    headers = {"HTTP_X_TENANT": tenant_acme.slug}
    api_client.post(reverse("platform_modules:module-install", args=["crm"]), **headers)
    enable = api_client.post(
        reverse("platform_modules:module-enable", args=["accounting"]), **headers,
    )
    assert enable.status_code == 200
    enable = api_client.post(
        reverse("platform_modules:module-enable", args=["crm"]), **headers,
    )
    assert enable.json()["status"] == ModuleStatus.ENABLED
    disable = api_client.post(
        reverse("platform_modules:module-disable", args=["crm"]), **headers,
    )
    assert disable.json()["status"] == ModuleStatus.DISABLED


@pytest.mark.django_db
def test_flag_endpoints(
    api_client, crm_stack, tenant_acme, acme_tree, alice,
):
    alice.is_superuser = True
    alice.save()
    api_client.force_login(alice)
    headers = {"HTTP_X_TENANT": tenant_acme.slug}
    api_client.post(reverse("platform_modules:module-install", args=["crm"]), **headers)
    api_client.post(reverse("platform_modules:module-enable", args=["accounting"]), **headers)
    api_client.post(reverse("platform_modules:module-enable", args=["crm"]), **headers)
    set_resp = api_client.post(
        reverse("platform_modules:flag-set", args=["crm", "lead_scoring"]),
        {"enabled": True},
        format="json",
        **headers,
    )
    assert set_resp.status_code == 200
    listing = api_client.get(
        reverse("platform_modules:flag-list", args=["crm"]), **headers,
    )
    flags = {f["name"]: f for f in listing.json()["results"]}
    assert flags["lead_scoring"]["tenant_enabled"] is True
    assert FeatureFlag.objects.filter(tenant=tenant_acme).count() == 1


@pytest.mark.django_db
def test_module_detail_404(api_client, _module_registry, tenant_acme, acme_tree, alice):
    alice.is_superuser = True
    alice.save()
    api_client.force_login(alice)
    resp = api_client.get(
        reverse("platform_modules:module-detail", args=["nope"]),
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert resp.status_code == 404
