"""Tests for symbolic contradiction detection and reporting."""

from __future__ import annotations

import logging

import networkx as nx
import numpy as np
import pytest

from hieromem_core.memory import Memory
from hieromem_core.symbolic import GraphReasoner


def expect(condition: bool, message: str | None = None) -> None:
    """Fail the test when *condition* is false without using ``assert``."""

    if not condition:
        pytest.fail(message or "Expectation failed", pytrace=True)


def _graph_with_cycle() -> nx.DiGraph:
    graph = nx.DiGraph()
    graph.add_node("alice", type="person")
    graph.add_node("bob", type="person")
    graph.add_edge("alice", "bob", relation="parent_of")
    graph.add_edge("bob", "alice", relation="parent_of")
    return graph


def _graph_with_conflicting_type() -> nx.DiGraph:
    graph = nx.DiGraph()
    graph.add_node("alice", type="location")
    return graph


def _graph_with_same_as_conflict() -> nx.DiGraph:
    graph = nx.DiGraph()
    graph.add_node("carol", type="vehicle")
    graph.add_node("carol_alias", type="person")
    graph.add_edge("carol", "carol_alias", relation="same_as")
    return graph


def _graph_with_cardinality_conflict() -> nx.DiGraph:
    graph = nx.DiGraph()
    graph.add_node("alice", type="person")
    graph.add_node("bob", type="person")
    graph.add_node("carol", type="person")
    graph.add_edge("alice", "bob", relation="married_to")
    graph.add_edge("alice", "carol", relation="married_to")
    return graph


def test_graph_reasoner_detects_symbolic_contradictions() -> None:
    reasoner = GraphReasoner()
    report = reasoner.inconsistencies([
        _graph_with_cycle(),
        _graph_with_conflicting_type(),
        _graph_with_same_as_conflict(),
        _graph_with_cardinality_conflict(),
    ])

    metrics = report.to_dict()
    expect(metrics["parent_of_cycles"] == 1, "should count parent_of cycles")
    expect(metrics["semantic_cycle_count"] == 1, "should count semantic cycles")
    expect(metrics["attribute_conflicts"] == 1, "should count attribute conflicts")
    expect(metrics["equivalence_conflicts"] == 1, "should count equivalence conflicts")
    expect(metrics["incompatible_relation_conflicts"] == 1,
           "should count incompatible relations")
    expect(metrics["mutually_exclusive_attribute_conflicts"] == 0,
           "should have no mutually exclusive attribute conflicts")
    expect(metrics["domain_specific_conflicts"] == 0,
           "domain specific conflicts should be zero")
    expect(metrics["relationship_cardinality_conflicts"] == 1,
           "should detect cardinality conflicts")
    expect(metrics["total_contradictions"] == 5, "should tally all contradictions")

    violations = metrics["violations"]
    expect(bool(violations["incompatible_relations"]),
           "violations should include incompatible relations")
    per_episode = violations["by_episode"]
    expect({
        "graph_0",
        "graph_1",
        "graph_2",
        "graph_3"}.issubset(
        per_episode.keys()),
           "violations should be recorded per episode")
    expect(
        any(v["type"] == "relation_cardinality" for v in per_episode["graph_3"]) and
        any(v["type"] == "attribute_conflict" for v in per_episode["graph_1"]) and
        any(v["type"] == "equivalence_conflict" for v in per_episode["graph_2"]) and
        any(v["type"] == "semantic_cycle" for v in per_episode["graph_0"]),
        "each graph should capture its expected violation type")


def test_memory_reports_namespace_contradictions() -> None:
    memory = Memory(enable_multi_tenant=True)

    memory.insert(
        np.array(
            [1.0],
            dtype=np.float32),
        _graph_with_cycle(),
        namespace="tenant_a")
    memory.insert(
        np.array(
            [0.5],
            dtype=np.float32),
        _graph_with_conflicting_type(),
        namespace="tenant_a")
    memory.insert(
        np.array(
            [0.25],
            dtype=np.float32),
        _graph_with_same_as_conflict(),
        namespace="tenant_a")
    memory.insert(
        np.array(
            [0.9],
            dtype=np.float32),
        _graph_with_cardinality_conflict(),
        namespace="tenant_a")

    clean_graph = nx.DiGraph()
    clean_graph.add_node("delta", type="person")
    memory.insert(
        np.array(
            [0.75],
            dtype=np.float32),
        clean_graph,
        namespace="tenant_b")

    tenant_a_metrics = memory.symbolic_inconsistency_metrics(
        namespace="tenant_a")
    expect(tenant_a_metrics["namespace"] == "tenant_a",
           "namespace should be preserved")
    expect(tenant_a_metrics["parent_of_cycles"] == 1,
           "should count parent cycles per namespace")
    expect(tenant_a_metrics["attribute_conflicts"] == 1,
           "should count attribute conflicts per namespace")
    expect(tenant_a_metrics["equivalence_conflicts"] == 1,
           "should count equivalence conflicts per namespace")
    expect(tenant_a_metrics["incompatible_relation_conflicts"] == 1,
           "should count incompatible relation conflicts per namespace")
    expect(tenant_a_metrics["relationship_cardinality_conflicts"] == 1,
           "should count cardinality conflicts per namespace")
    expect(tenant_a_metrics["total_contradictions"] == 5,
           "should total contradictions for tenant A")
    expect(bool(tenant_a_metrics["violations"]["incompatible_relations"]),
           "violations should include incompatible relations")
    expect(len(tenant_a_metrics["violations"]["by_episode"]) == 4,
           "should report violations for all tenant A episodes")

    summary = memory.symbolic_inconsistency_by_namespace()
    expect(summary["tenant_a"]["total_contradictions"] == 5,
           "summary should include tenant A totals")
    expect(summary["tenant_b"]["total_contradictions"] == 0,
           "tenant B should remain contradiction-free")


def test_merge_graphs_prefers_high_trust_edges(
        caplog: pytest.LogCaptureFixture) -> None:
    reasoner = GraphReasoner()
    caplog.set_level(logging.WARNING, logger="GraphReasoner")

    g1 = nx.DiGraph()
    g1.add_edge("alice", "bob", relation="knows", status="confirmed")

    g2 = nx.DiGraph()
    g2.add_edge("alice", "bob", relation="knows", status="refuted")

    merged = reasoner.merge_graphs(
        [
            (g1, {"episode_id": "ep1", "trust": 0.9}),
            (g2, {"episode_id": "ep2", "trust": 0.1}),
        ]
    )

    expect(merged.edges["alice", "bob"]["status"] == "confirmed",
           "higher trust edge should be preferred")
    expect(set(merged.edges["alice", "bob"]["provenance"]) == {"ep1", "ep2"},
           "provenance should merge episode ids")

    conflicts = merged.graph.get("merge_conflicts", [])
    expect(conflicts and conflicts[0]["attribute"] == "status",
           "conflict should be recorded for differing status")
    expect(any("status" in record.message for record in caplog.records),
           "conflict should be logged")


def test_merge_graphs_prefers_richer_provenance_when_trust_equal() -> None:
    reasoner = GraphReasoner()

    g1 = nx.DiGraph()
    g1.add_edge(
        "alice",
        "bob",
        relation="knows",
        status="inferred",
        provenance=["src1"])

    g2 = nx.DiGraph()
    g2.add_edge(
        "alice",
        "bob",
        relation="knows",
        status="verified",
        provenance=[
            "src2",
            "src3"])

    merged = reasoner.merge_graphs(
        [
            (g1, {"episode_id": "ep1", "trust": 0.6}),
            (g2, {"episode_id": "ep2", "trust": 0.6}),
        ]
    )

    expect(merged.edges["alice", "bob"]["status"] == "verified",
           "richer provenance should be preferred when trust equal")
    provenance = set(merged.edges["alice", "bob"]["provenance"])
    expect(provenance == {"ep1", "ep2", "src1", "src2", "src3"},
           "provenance should union episode and source identifiers")

    conflicts = merged.graph.get("merge_conflicts", [])
    expect(conflicts and conflicts[0]["chosen"]["value"] == "verified",
           "conflict resolution should reflect chosen value")
