"""
BIBIVIDI — BSS 2.0 Auto-assembly tests (TODO-20)

Tests for:
- _detect_input_type heuristic
- Library.build_type_graph
- _bfs_type_path BFS path finder
- Matcher.assemble_pipeline integration
- Proxy auto-assembly integration
"""
from __future__ import annotations

import numpy as np
import pytest

from bvd.engine.library import Library, Script
from bvd.engine.matcher import (
    AssemblyResult,
    Matcher,
    _bfs_type_path,
    _detect_input_type,
)

# ── Helpers ────────────────────────────────────────────────────────────────────

def _typed_script(sid: str, inp: str, out: str, status: str = "active") -> Script:
    return Script(
        id=sid,
        name=sid,
        description=f"{inp} → {out}",
        code="",
        inputs_type=inp,
        outputs_type=out,
        status=status,
        embedding=np.zeros(384, dtype=np.float32).tobytes(),
    )


# ── _detect_input_type ─────────────────────────────────────────────────────────

def test_detect_pdf_magic_bytes():
    assert _detect_input_type("%PDF-1.4 ...some content") == "pdf"


def test_detect_pdf_base64():
    # "JVBERi0" is the base64 encoding of "%PDF-"
    assert _detect_input_type("JVBERi0xLjQK...") == "pdf"


def test_detect_diff():
    diff = "--- a/file.py\n+++ b/file.py\n@@ -1,3 +1,4 @@\n line"
    assert _detect_input_type(diff) == "diff"


def test_detect_csv():
    csv = "name,age,city\nAlice,30,Paris\nBob,25,Lyon\nCarol,35,Nice"
    assert _detect_input_type(csv) == "csv"


def test_detect_json_object():
    assert _detect_input_type('{"key": "value", "n": 42}') == "json"


def test_detect_json_array():
    assert _detect_input_type('[{"id": 1}, {"id": 2}]') == "json"


def test_detect_xml():
    assert _detect_input_type('<?xml version="1.0"?><root><item>x</item></root>') == "xml"


def test_detect_xml_tag():
    assert _detect_input_type("<document><section>text</section></document>") == "xml"


def test_detect_plain_text():
    assert _detect_input_type("Please summarize this article about AI trends.") == "plain_text"


def test_detect_invalid_json_not_json():
    """Malformed JSON that starts with { but doesn't parse → not detected as json."""
    result = _detect_input_type("{bad json here without closing brace")
    # Might be plain_text since JSON parse fails and no other markers
    assert result in ("plain_text", "json")  # implementation may vary; just not pdf/diff/csv


# ── Library.build_type_graph ───────────────────────────────────────────────────

def test_build_type_graph_basic():
    scripts = [
        _typed_script("a", "pdf", "text"),
        _typed_script("b", "text", "plain_text"),
        _typed_script("c", "csv", "plain_text"),
    ]
    graph = Library.build_type_graph(scripts)
    assert "pdf" in graph
    assert any(out == "text" for out, _ in graph["pdf"])
    assert "csv" in graph
    assert any(out == "plain_text" for out, _ in graph["csv"])


def test_build_type_graph_excludes_pending():
    scripts = [
        _typed_script("active", "csv", "plain_text", status="active"),
        _typed_script("pending", "pdf", "plain_text", status="pending"),
    ]
    graph = Library.build_type_graph(scripts)
    assert "csv" in graph
    assert "pdf" not in graph


def test_build_type_graph_excludes_untyped():
    s = Script(id="no-type", name="no-type", description="", code="")
    graph = Library.build_type_graph([s])
    assert graph == {}


def test_build_type_graph_excludes_identity():
    s = _typed_script("identity", "csv", "csv")
    graph = Library.build_type_graph([s])
    assert graph == {}


def test_build_type_graph_multiple_edges_same_input():
    scripts = [
        _typed_script("a", "pdf", "plain_text"),
        _typed_script("b", "pdf", "structured_text"),
    ]
    graph = Library.build_type_graph(scripts)
    assert len(graph["pdf"]) == 2


# ── _bfs_type_path ─────────────────────────────────────────────────────────────

def _make_graph_and_scripts():
    """pdf → text → plain_text (2-hop path)"""
    s_pdf = _typed_script("pdf-extractor", "pdf", "text")
    s_txt = _typed_script("text-condenser", "text", "plain_text")
    graph = {
        "pdf": [("text", s_pdf)],
        "text": [("plain_text", s_txt)],
    }
    return graph, s_pdf, s_txt


def test_bfs_direct_path():
    s = _typed_script("csv-sum", "csv", "plain_text")
    graph = {"csv": [("plain_text", s)]}
    path = _bfs_type_path(graph, "csv", "plain_text")
    assert path == [s]


def test_bfs_two_hop_path():
    graph, s_pdf, s_txt = _make_graph_and_scripts()
    path = _bfs_type_path(graph, "pdf", "plain_text")
    assert path == [s_pdf, s_txt]


def test_bfs_shortest_path_chosen():
    """When multiple paths exist, BFS returns the shortest."""
    s_direct = _typed_script("direct", "pdf", "plain_text")
    s_a = _typed_script("step-a", "pdf", "intermediate")
    s_b = _typed_script("step-b", "intermediate", "plain_text")
    graph = {
        "pdf": [("plain_text", s_direct), ("intermediate", s_a)],
        "intermediate": [("plain_text", s_b)],
    }
    path = _bfs_type_path(graph, "pdf", "plain_text")
    assert path == [s_direct]


def test_bfs_no_path_returns_none():
    graph, _, _ = _make_graph_and_scripts()
    assert _bfs_type_path(graph, "xml", "plain_text") is None


def test_bfs_start_equals_target():
    graph, _, _ = _make_graph_and_scripts()
    assert _bfs_type_path(graph, "plain_text", "plain_text") == []


def test_bfs_max_depth_exceeded():
    """Paths longer than max_depth are rejected."""
    # Build a 3-hop chain but set max_depth=2
    s1 = _typed_script("s1", "a", "b")
    s2 = _typed_script("s2", "b", "c")
    s3 = _typed_script("s3", "c", "plain_text")
    graph = {"a": [("b", s1)], "b": [("c", s2)], "c": [("plain_text", s3)]}
    assert _bfs_type_path(graph, "a", "plain_text", max_depth=2) is None


def test_bfs_cycle_safe():
    """BFS does not loop on cyclic graphs."""
    s1 = _typed_script("s1", "a", "b")
    s2 = _typed_script("s2", "b", "a")   # cycle back
    graph = {"a": [("b", s1)], "b": [("a", s2)]}
    result = _bfs_type_path(graph, "a", "plain_text")
    assert result is None


# ── Matcher.assemble_pipeline ──────────────────────────────────────────────────

@pytest.mark.asyncio
async def test_assemble_pipeline_plain_text_returns_none():
    m = Matcher()
    m._index = []
    m._index_dirty = False
    result = await m.assemble_pipeline("This is a plain English question.")
    assert result is None


@pytest.mark.asyncio
async def test_assemble_pipeline_no_typed_scripts_returns_none():
    m = Matcher()
    # Script with no typed I/O in the index
    emb = np.zeros(384, dtype=np.float32).tobytes()
    s = Script(id="s", name="s", description="", code="", embedding=emb)
    m._index = [("s", np.zeros(384, dtype=np.float32), s)]
    m._index_dirty = False
    result = await m.assemble_pipeline("%PDF-1.4 some contract text...")
    assert result is None


@pytest.mark.asyncio
async def test_assemble_pipeline_finds_direct_path():
    m = Matcher()
    s_pdf = _typed_script("pdf-extractor", "pdf", "plain_text")
    m._index = [("pdf-extractor", np.zeros(384, dtype=np.float32), s_pdf)]
    m._index_dirty = False

    result = await m.assemble_pipeline("%PDF-1.4 CONTRACT CONTENT HERE")
    assert result is not None
    assert isinstance(result, AssemblyResult)
    assert result.input_type == "pdf"
    assert result.output_type == "plain_text"
    assert result.pipeline == [s_pdf]


@pytest.mark.asyncio
async def test_assemble_pipeline_finds_two_hop_path():
    m = Matcher()
    s_pdf = _typed_script("pdf-to-text", "pdf", "text")
    s_txt = _typed_script("text-to-plain", "text", "plain_text")
    m._index = [
        ("pdf-to-text", np.zeros(384, dtype=np.float32), s_pdf),
        ("text-to-plain", np.zeros(384, dtype=np.float32), s_txt),
    ]
    m._index_dirty = False

    result = await m.assemble_pipeline("%PDF-1.4 some contract")
    assert result is not None
    assert result.pipeline == [s_pdf, s_txt]


@pytest.mark.asyncio
async def test_assemble_pipeline_no_path_returns_none():
    m = Matcher()
    # Only XML skill but prompt is a diff
    s_xml = _typed_script("xml-condenser", "xml", "plain_text")
    m._index = [("xml-condenser", np.zeros(384, dtype=np.float32), s_xml)]
    m._index_dirty = False

    diff = "--- a/file.py\n+++ b/file.py\n@@ -1,3 +1,4 @@\n line"
    result = await m.assemble_pipeline(diff)
    assert result is None


# ── Proxy integration ──────────────────────────────────────────────────────────

@pytest.mark.asyncio
async def test_proxy_auto_assembly_disabled_by_default(monkeypatch):
    """With enable_auto_assembly=False, assemble_pipeline is never called."""
    import bvd.core.proxy as proxy_mod
    from bvd.config import settings

    called = []

    async def _fake_assemble(prompt):
        called.append(prompt)
        return None

    monkeypatch.setattr(settings, "enable_auto_assembly", False)
    monkeypatch.setattr(proxy_mod.matcher, "assemble_pipeline", _fake_assemble)

    # Verify the flag is off (no call expected)
    assert not settings.enable_auto_assembly
    assert called == []


@pytest.mark.asyncio
async def test_proxy_auto_assembly_skips_generator_when_pipeline_assembled(monkeypatch):
    """When auto-assembly succeeds, generator.maybe_generate is NOT called."""
    import bvd.core.proxy as proxy_mod
    from bvd.config import settings
    from bvd.engine.matcher import AssemblyResult
    from bvd.engine.runner import ChainRunResult

    gen_calls = []

    monkeypatch.setattr(settings, "enable_auto_assembly", True)
    monkeypatch.setattr(settings, "generator_enabled", True)

    s = _typed_script("pdf-condenser", "pdf", "plain_text")
    assembly = AssemblyResult(pipeline=[s], input_type="pdf", output_type="plain_text")

    async def _fake_assemble(prompt):
        return assembly

    async def _fake_run_chain(chain_script, prompt, steps, *, bypass_feature_flag=False):
        return ChainRunResult(output="condensed", step_outputs=["condensed"], exit_code=0)

    async def _fake_generate(prompt):
        gen_calls.append(prompt)

    monkeypatch.setattr(proxy_mod.matcher, "assemble_pipeline", _fake_assemble)
    monkeypatch.setattr(proxy_mod, "run_chain", _fake_run_chain)
    monkeypatch.setattr(proxy_mod.generator, "maybe_generate", _fake_generate)

    # Simulate the auto-assembly branch logic directly
    prompt = "%PDF-1.4 contract"
    assembly_result = await proxy_mod.matcher.assemble_pipeline(prompt)
    assert assembly_result is not None

    # Generator should not have been called
    assert gen_calls == []
