"""Tests for orxaq.enterprise.tenant -- multi-tenant isolation."""

from __future__ import annotations

import pytest

from orxaq.enterprise.tenant import (
    QuotaExceededError,
    TenantAuditEntry,
    TenantAuditLog,
    TenantContext,
    TenantDAGProxy,
    TenantMiddleware,
    TenantResolution,
    TenantStore,
    get_current_tenant,
    set_current_tenant,
)
from orxaq.kernel.types import CausalEdge, Entity
from orxaq.kernel.world_dag import WorldDAG

# ── TenantContext ────────────────────────────────────────────


class TestTenantContext:
    def test_defaults(self):
        ctx = TenantContext(tenant_id="t1")
        assert ctx.tenant_id == "t1"
        assert ctx.tier == "free"
        assert ctx.max_entities == 100
        assert ctx.is_unlimited is False

    def test_enterprise_is_unlimited(self):
        ctx = TenantContext(tenant_id="t2", tier="enterprise")
        assert ctx.is_unlimited is True

    def test_frozen(self):
        ctx = TenantContext(tenant_id="t1")
        with pytest.raises(AttributeError):
            ctx.tenant_id = "t2"  # type: ignore[misc]


# ── Context Vars ─────────────────────────────────────────────


class TestContextVars:
    def test_get_set_current_tenant(self):
        assert get_current_tenant() is None
        ctx = TenantContext(tenant_id="cv-test")
        set_current_tenant(ctx)
        assert get_current_tenant() is ctx
        set_current_tenant(None)
        assert get_current_tenant() is None


# ── TenantStore ──────────────────────────────────────────────


class TestTenantStore:
    def test_create_and_get(self):
        store = TenantStore()
        ctx = store.create("t1", name="Acme")
        assert ctx.tenant_id == "t1"
        assert ctx.name == "Acme"
        assert store.get("t1") is ctx

    def test_create_duplicate_raises(self):
        store = TenantStore()
        store.create("t1")
        with pytest.raises(ValueError, match="already exists"):
            store.create("t1")

    def test_get_missing_returns_none(self):
        store = TenantStore()
        assert store.get("nope") is None

    def test_delete_existing(self):
        store = TenantStore()
        store.create("t1")
        assert store.delete("t1") is True
        assert store.get("t1") is None

    def test_delete_missing(self):
        store = TenantStore()
        assert store.delete("nope") is False

    def test_list_tenants_sorted(self):
        store = TenantStore()
        store.create("z-corp")
        store.create("a-corp")
        store.create("m-corp")
        ids = [t.tenant_id for t in store.list_tenants()]
        assert ids == ["a-corp", "m-corp", "z-corp"]

    def test_count(self):
        store = TenantStore()
        assert store.count() == 0
        store.create("t1")
        store.create("t2")
        assert store.count() == 2


# ── TenantDAGProxy ───────────────────────────────────────────


class TestTenantDAGProxy:
    @pytest.fixture()
    def dag(self):
        return WorldDAG()

    @pytest.fixture()
    def tenant_a(self):
        return TenantContext(tenant_id="a", max_entities=3, max_edges=3)

    @pytest.fixture()
    def tenant_b(self):
        return TenantContext(tenant_id="b", max_entities=3, max_edges=3)

    def test_add_and_get_entity(self, dag, tenant_a):
        proxy = TenantDAGProxy(dag, tenant_a)
        proxy.add_entity(Entity(entity_id="x", name="X"))
        ent = proxy.get_entity("x")
        assert ent is not None
        assert ent.entity_id == "x"
        assert ent.name == "X"

    def test_entity_isolation(self, dag, tenant_a, tenant_b):
        pa = TenantDAGProxy(dag, tenant_a)
        pb = TenantDAGProxy(dag, tenant_b)
        pa.add_entity(Entity(entity_id="shared_name", name="A's"))
        pb.add_entity(Entity(entity_id="shared_name", name="B's"))
        assert pa.get_entity("shared_name").name == "A's"
        assert pb.get_entity("shared_name").name == "B's"

    def test_entity_not_visible_across_tenants(self, dag, tenant_a, tenant_b):
        pa = TenantDAGProxy(dag, tenant_a)
        pb = TenantDAGProxy(dag, tenant_b)
        pa.add_entity(Entity(entity_id="only_a", name="Only A"))
        assert pb.get_entity("only_a") is None

    def test_remove_entity(self, dag, tenant_a):
        proxy = TenantDAGProxy(dag, tenant_a)
        proxy.add_entity(Entity(entity_id="x", name="X"))
        proxy.remove_entity("x")
        assert proxy.get_entity("x") is None

    def test_add_edge(self, dag, tenant_a):
        proxy = TenantDAGProxy(dag, tenant_a)
        proxy.add_entity(Entity(entity_id="x", name="X"))
        proxy.add_entity(Entity(entity_id="y", name="Y"))
        proxy.add_edge(CausalEdge(source="x", target="y"))
        assert proxy.edge_count() == 1

    def test_entity_count(self, dag, tenant_a, tenant_b):
        pa = TenantDAGProxy(dag, tenant_a)
        pb = TenantDAGProxy(dag, tenant_b)
        pa.add_entity(Entity(entity_id="a1", name="A1"))
        pa.add_entity(Entity(entity_id="a2", name="A2"))
        pb.add_entity(Entity(entity_id="b1", name="B1"))
        assert pa.entity_count() == 2
        assert pb.entity_count() == 1

    def test_list_entities(self, dag, tenant_a):
        proxy = TenantDAGProxy(dag, tenant_a)
        proxy.add_entity(Entity(entity_id="x", name="X"))
        proxy.add_entity(Entity(entity_id="y", name="Y"))
        entities = proxy.list_entities()
        ids = {e.entity_id for e in entities}
        assert ids == {"x", "y"}

    def test_entity_quota_exceeded(self, dag, tenant_a):
        proxy = TenantDAGProxy(dag, tenant_a)  # max_entities=3
        proxy.add_entity(Entity(entity_id="a", name="A"))
        proxy.add_entity(Entity(entity_id="b", name="B"))
        proxy.add_entity(Entity(entity_id="c", name="C"))
        with pytest.raises(QuotaExceededError, match="entity quota"):
            proxy.add_entity(Entity(entity_id="d", name="D"))

    def test_edge_quota_exceeded(self, dag):
        tenant = TenantContext(tenant_id="eq", max_entities=10, max_edges=3)
        proxy = TenantDAGProxy(dag, tenant)
        for name in ("a", "b", "c", "d"):
            proxy.add_entity(Entity(entity_id=name, name=name.upper()))
        proxy.add_edge(CausalEdge(source="a", target="b"))
        proxy.add_edge(CausalEdge(source="b", target="c"))
        proxy.add_edge(CausalEdge(source="c", target="d"))
        with pytest.raises(QuotaExceededError, match="edge quota"):
            proxy.add_edge(CausalEdge(source="a", target="d"))

    def test_enterprise_bypasses_quota(self, dag):
        tenant = TenantContext(tenant_id="e", tier="enterprise", max_entities=1)
        proxy = TenantDAGProxy(dag, tenant)
        proxy.add_entity(Entity(entity_id="a", name="A"))
        proxy.add_entity(Entity(entity_id="b", name="B"))  # no error
        assert proxy.entity_count() == 2


# ── TenantMiddleware ─────────────────────────────────────────


class TestTenantMiddleware:
    def test_resolve_mapped_identity(self):
        store = TenantStore()
        store.create("t1", name="Acme")
        mw = TenantMiddleware(store)
        mw.map_identity("key_abc", "t1")
        result = mw.resolve("key_abc")
        assert result.resolved is True
        assert result.tenant is not None
        assert result.tenant.tenant_id == "t1"
        assert get_current_tenant() is result.tenant
        mw.clear()

    def test_resolve_unmapped_identity(self):
        store = TenantStore()
        mw = TenantMiddleware(store)
        result = mw.resolve("unknown")
        assert result.resolved is False
        assert result.error == "no_tenant_mapping"

    def test_resolve_mapped_to_deleted_tenant(self):
        store = TenantStore()
        store.create("t1")
        mw = TenantMiddleware(store)
        mw.map_identity("key_abc", "t1")
        store.delete("t1")
        result = mw.resolve("key_abc")
        assert result.resolved is False
        assert result.error == "tenant_not_found"

    def test_unmap_identity(self):
        store = TenantStore()
        mw = TenantMiddleware(store)
        mw.map_identity("key_abc", "t1")
        assert mw.unmap_identity("key_abc") is True
        assert mw.unmap_identity("key_abc") is False

    def test_clear_resets_context(self):
        store = TenantStore()
        store.create("t1")
        mw = TenantMiddleware(store)
        mw.map_identity("key_abc", "t1")
        mw.resolve("key_abc")
        assert get_current_tenant() is not None
        mw.clear()
        assert get_current_tenant() is None


# ── TenantAuditLog ───────────────────────────────────────────


class TestTenantAuditLog:
    def test_record_and_retrieve(self):
        log = TenantAuditLog()
        entry = TenantAuditEntry(tenant_id="t1", action="create_entity", entity_id="x")
        log.record(entry)
        entries = log.entries_for("t1")
        assert len(entries) == 1
        assert entries[0].action == "create_entity"

    def test_entries_isolated_by_tenant(self):
        log = TenantAuditLog()
        log.record(TenantAuditEntry(tenant_id="t1", action="a"))
        log.record(TenantAuditEntry(tenant_id="t2", action="b"))
        assert len(log.entries_for("t1")) == 1
        assert len(log.entries_for("t2")) == 1
        assert log.entries_for("t1")[0].action == "a"

    def test_max_entries_retention(self):
        log = TenantAuditLog(max_entries=3)
        for i in range(5):
            log.record(TenantAuditEntry(tenant_id="t1", action=f"action_{i}"))
        entries = log.entries_for("t1")
        assert len(entries) == 3
        assert entries[0].action == "action_2"  # oldest retained

    def test_count_for(self):
        log = TenantAuditLog()
        log.record(TenantAuditEntry(tenant_id="t1", action="a"))
        log.record(TenantAuditEntry(tenant_id="t1", action="b"))
        assert log.count_for("t1") == 2
        assert log.count_for("t2") == 0

    def test_purge(self):
        log = TenantAuditLog()
        log.record(TenantAuditEntry(tenant_id="t1", action="a"))
        log.record(TenantAuditEntry(tenant_id="t1", action="b"))
        assert log.purge("t1") == 2
        assert log.count_for("t1") == 0
        assert log.purge("t1") == 0

    def test_timestamp_auto_set(self):
        entry = TenantAuditEntry(tenant_id="t1", action="test")
        assert entry.timestamp > 0


# ── TenantResolution ─────────────────────────────────────────


class TestTenantResolution:
    def test_success(self):
        ctx = TenantContext(tenant_id="t1")
        r = TenantResolution(resolved=True, tenant=ctx)
        assert r.resolved is True
        assert r.tenant is ctx

    def test_failure(self):
        r = TenantResolution(resolved=False, error="no_tenant_mapping")
        assert r.resolved is False
        assert r.tenant is None


# ── Targeted coverage (lines 135, 140, 147) ─────────────────


class TestTenantDAGProxyHelpers:
    """Cover lines 135, 140, 147: tenant property and scoping helpers."""

    def test_tenant_property(self):
        """Cover line 135: access .tenant property."""
        dag = WorldDAG()
        ctx = TenantContext(tenant_id="test_t")
        proxy = TenantDAGProxy(dag, ctx)
        assert proxy.tenant.tenant_id == "test_t"

    def test_scoped_id_already_prefixed(self):
        """Cover line 140: _scoped_id when already prefixed."""
        dag = WorldDAG()
        ctx = TenantContext(tenant_id="t1")
        proxy = TenantDAGProxy(dag, ctx)
        assert proxy._scoped_id("t1:entity_x") == "t1:entity_x"

    def test_unscoped_id_no_prefix(self):
        """Cover line 147: _unscoped_id when not prefixed."""
        dag = WorldDAG()
        ctx = TenantContext(tenant_id="t1")
        proxy = TenantDAGProxy(dag, ctx)
        assert proxy._unscoped_id("raw_id") == "raw_id"
