"""Tests for generate/graph_constraint.py — PropositionGraph as AdmissibilityRegion.

ADR-0046.
"""

from __future__ import annotations

import pytest
import numpy as np

from generate.graph_planner import GraphEdge, GraphNode, PropositionGraph, Relation
from generate.graph_constraint import build_graph_constraint
from generate.admissibility import AdmissibilityRegion
from generate.intent import IntentTag


@pytest.fixture()
def vocab():
    from language_packs import load_pack
    _manifest, manifold = load_pack("en_core_cognition_v1")
    return manifold


def _node(node_id, subject, obj):
    return GraphNode(
        node_id=node_id,
        subject=subject,
        predicate="addresses",
        obj=obj,
        source_intent=IntentTag.DEFINITION,
    )


class TestBuildGraphConstraint:
    def test_returns_admissibility_region(self, vocab):
        graph = PropositionGraph().add_node(_node("p0", "light", "truth"))
        region = build_graph_constraint(graph, vocab)
        assert isinstance(region, AdmissibilityRegion)

    def test_non_trivial_constraint(self, vocab):
        """allowed_indices must be a strict subset of the full vocabulary."""
        graph = PropositionGraph().add_node(_node("p0", "light", "truth"))
        region = build_graph_constraint(graph, vocab, top_k=8)
        assert region.allowed_indices is not None
        assert len(region.allowed_indices) < len(vocab)

    def test_allowed_indices_positive_cga_inner(self, vocab):
        """Every allowed index must score positive cga_inner against at least one anchor."""
        from algebra.cga import cga_inner
        graph = PropositionGraph().add_node(_node("p0", "light", "truth"))
        region = build_graph_constraint(graph, vocab, top_k=8)
        assert region.allowed_indices is not None
        light_v = np.asarray(vocab.get_versor("light"), dtype=np.float32)
        truth_v = np.asarray(vocab.get_versor("truth"), dtype=np.float32)
        anchors = [light_v, truth_v]
        for idx in region.allowed_indices:
            scores = [
                float(cga_inner(np.asarray(vocab.get_versor_at(int(idx)), dtype=np.float32), a))
                for a in anchors
            ]
            assert max(scores) > 0.0, f"Index {idx} has non-positive CGA score against all anchors"

    def test_empty_graph_returns_unconstrained(self, vocab):
        """An empty graph degrades gracefully to an unconstrained region."""
        region = build_graph_constraint(PropositionGraph(), vocab)
        assert region.allowed_indices is None
        assert "unconstrained" in region.label

    def test_two_node_graph_unions_neighbourhoods(self, vocab):
        """A two-node graph produces a larger allowed set than a one-node graph."""
        graph_one = PropositionGraph().add_node(_node("p0", "light", "truth"))
        graph_two = (
            PropositionGraph()
            .add_node(_node("p0", "light", "truth"))
            .add_node(_node("p1", "word", "life"))
        )
        region_one = build_graph_constraint(graph_one, vocab, top_k=4)
        region_two = build_graph_constraint(graph_two, vocab, top_k=4)
        count_one = len(region_one.allowed_indices) if region_one.allowed_indices is not None else len(vocab)
        count_two = len(region_two.allowed_indices) if region_two.allowed_indices is not None else len(vocab)
        assert count_two >= count_one

    def test_label_encodes_root_node_ids(self, vocab):
        """The region label must encode the graph's root node IDs."""
        graph = PropositionGraph().add_node(_node("p0", "light", "truth"))
        region = build_graph_constraint(graph, vocab)
        assert "p0" in region.label

    def test_round_trip_with_generate(self, vocab):
        """The region produced by build_graph_constraint can be fed to generate() without raising."""
        from field.state import FieldState
        from generate.stream import generate
        from persona.motor import PersonaMotor

        graph = PropositionGraph().add_node(_node("p0", "light", "truth"))
        region = build_graph_constraint(graph, vocab, top_k=8)

        F0 = np.asarray(vocab.get_versor("light"), dtype=np.float64)
        state = FieldState(F=F0, node=vocab.index_of("light"), step=0)
        persona = PersonaMotor.identity()

        result = generate(
            state,
            vocab,
            persona,
            max_tokens=4,
            region=region,
        )
        assert result.tokens is not None

    def test_oov_nodes_degrade_gracefully(self, vocab):
        """A graph whose nodes are all OOV returns an unconstrained region."""
        graph = PropositionGraph().add_node(
            GraphNode(
                node_id="p0",
                subject="xyzzy_not_a_word",
                predicate="quux",
                obj="zork_also_not_a_word",
                source_intent=IntentTag.UNKNOWN,
            )
        )
        region = build_graph_constraint(graph, vocab)
        assert region.allowed_indices is None
