"""
BIBIVIDI — Runner tests
"""
import pytest

from bvd.engine.library import Script
from bvd.engine.runner import MAX_STDOUT_BYTES, run_chain, run_script


def _make_script(code: str) -> Script:
    return Script(id="test", name="Test", description="test script", code=code)


@pytest.mark.asyncio
async def test_runner_basic_output():
    s = _make_script("print('hello bibividi')")
    result = await run_script(s, "ignored prompt")
    assert result.output == "hello bibividi"
    assert result.exit_code == 0
    assert not result.timed_out


@pytest.mark.asyncio
async def test_runner_reads_prompt():
    s = _make_script("print(f'got: {PROMPT[:5]}')")
    result = await run_script(s, "abcde xyz")
    assert "got: abcde" in result.output


@pytest.mark.asyncio
async def test_runner_blocked_import():
    s = _make_script("import os\nprint(os.getcwd())")
    result = await run_script(s, "test")
    assert result.exit_code != 0


@pytest.mark.asyncio
async def test_runner_timeout(monkeypatch):
    from bvd import config
    monkeypatch.setattr(config.settings, "max_script_runtime_s", 0.5)
    s = _make_script("while True: pass  # infinite loop — no imports needed")
    result = await run_script(s, "test")
    assert result.timed_out


@pytest.mark.asyncio
async def test_runner_fallback_on_error():
    """If script crashes, runner returns original prompt."""
    s = _make_script("raise ValueError('boom')")
    result = await run_script(s, "original prompt")
    assert result.output == "original prompt"


@pytest.mark.asyncio
async def test_runner_pending_skill_passes_through():
    """Pending skills must NOT execute — original prompt is returned unchanged."""
    s = _make_script("print('condensed output')")
    s.status = "pending"
    result = await run_script(s, "original prompt")
    assert result.output == "original prompt"
    assert result.stderr == "PENDING"
    assert result.exit_code == 0


@pytest.mark.asyncio
async def test_runner_active_skill_executes():
    """Active skills (default) execute normally."""
    s = _make_script("print('condensed output')")
    assert s.status == "active"
    result = await run_script(s, "original prompt")
    assert result.output == "condensed output"


@pytest.mark.asyncio
async def test_runner_stdout_capped():
    """Runaway scripts producing huge output are truncated at MAX_STDOUT_BYTES."""
    # Generate output well beyond the cap (1 MB > 512 KB)
    s = _make_script("print('x' * 1_100_000)")
    result = await run_script(s, "test")
    assert len(result.output.encode()) <= MAX_STDOUT_BYTES
    assert result.exit_code == 0


# ── BSS 1.1 chain tests ───────────────────────────────────────────────────────

def _make_step(step_id: str, code: str) -> Script:
    return Script(id=step_id, name=step_id, description=step_id, code=code)


def _make_chain(chain_ids: list[str], finalizer_code: str = "") -> Script:
    return Script(
        id="test-chain",
        name="Test Chain",
        description="test chain",
        code=finalizer_code,
        chain_ids=chain_ids,
    )


@pytest.mark.asyncio
async def test_chain_happy_path(monkeypatch):
    """3 steps succeed sequentially; final output is shorter than original."""
    from bvd import config
    monkeypatch.setattr(config.settings, "enable_chains", True)

    # "aaaa...bbbb...cccc..." (1000 chars) → "HELLO" after 3 steps
    long_prompt = "a" * 1000
    step_a = _make_step("step-a", "print('reduced')")
    step_b = _make_step("step-b", "print(PROMPT.upper())")
    step_c = _make_step("step-c", "print('short')")
    chain = _make_chain(["step-a", "step-b", "step-c"])

    result = await run_chain(chain, long_prompt, [step_a, step_b, step_c])

    assert result.exit_code == 0
    assert not result.timed_out
    assert result.output == "short"
    assert len(result.step_outputs) == 3


@pytest.mark.asyncio
async def test_chain_step_failure(monkeypatch):
    """Step 2 of 3 exits non-zero → falls back to original prompt."""
    from bvd import config
    monkeypatch.setattr(config.settings, "enable_chains", True)

    prompt = "original prompt"
    step_a = _make_step("step-a", "print('ok')")
    step_b = _make_step("step-b", "raise ValueError('boom')")
    step_c = _make_step("step-c", "print('never reached')")
    chain = _make_chain(["step-a", "step-b", "step-c"])

    result = await run_chain(chain, prompt, [step_a, step_b, step_c])

    assert result.output == prompt
    assert result.exit_code != 0
    assert result.failed_step == 1
    assert len(result.step_outputs) == 1  # only step-a succeeded


@pytest.mark.asyncio
async def test_chain_timeout(monkeypatch):
    """Total chain timeout fires → fallback + timed_out=True."""
    from bvd import config
    monkeypatch.setattr(config.settings, "enable_chains", True)
    monkeypatch.setattr(config.settings, "max_chain_runtime_s", 0.3)
    monkeypatch.setattr(config.settings, "max_script_runtime_s", 30.0)

    prompt = "original prompt"
    step = _make_step("inf-loop", "while True: pass")
    chain = _make_chain(["inf-loop"])

    result = await run_chain(chain, prompt, [step])

    assert result.timed_out
    assert result.output == prompt


@pytest.mark.asyncio
async def test_chain_cycle_runtime(monkeypatch):
    """Duplicate skill_id in step list detected at runtime → fallback."""
    from bvd import config
    monkeypatch.setattr(config.settings, "enable_chains", True)

    prompt = "original prompt"
    step_a = _make_step("step-a", "print('first')")
    step_a_dup = _make_step("step-a", "print('duplicate')")  # same ID
    chain = _make_chain(["step-a", "step-a"])

    result = await run_chain(chain, prompt, [step_a, step_a_dup])

    assert result.output == prompt
    assert result.exit_code == 0


@pytest.mark.asyncio
async def test_chain_missing_dep(monkeypatch):
    """None in step_scripts (dep not found in library) → fallback."""
    from bvd import config
    monkeypatch.setattr(config.settings, "enable_chains", True)

    prompt = "original prompt"
    step_a = _make_step("step-a", "print('ok')")
    chain = _make_chain(["step-a", "missing-dep"])

    result = await run_chain(chain, prompt, [step_a, None])

    assert result.output == prompt
    assert result.failed_step == 1


@pytest.mark.asyncio
async def test_chain_empty_step_output(monkeypatch):
    """Step that prints nothing → next step receives previous step's output."""
    from bvd import config
    monkeypatch.setattr(config.settings, "enable_chains", True)

    long_prompt = "x" * 500
    step_a = _make_step("step-a", "print('condensed')")
    step_b = _make_step("step-b", "")        # prints nothing — empty stdout
    step_c = _make_step("step-c", "print(PROMPT)")  # should receive step-a's output
    chain = _make_chain(["step-a", "step-b", "step-c"])

    result = await run_chain(chain, long_prompt, [step_a, step_b, step_c])

    assert result.exit_code == 0
    # step-b passed step-a's "condensed" through; step-c echoed it
    assert result.output == "condensed"


@pytest.mark.asyncio
async def test_chain_larger_than_original(monkeypatch):
    """Chain output exceeds original prompt size → fallback to original."""
    from bvd import config
    monkeypatch.setattr(config.settings, "enable_chains", True)

    short_prompt = "hi"  # 2 chars
    step = _make_step("expander", "print('x' * 1000)")
    chain = _make_chain(["expander"])

    result = await run_chain(chain, short_prompt, [step])

    assert result.output == short_prompt  # fell back
    assert result.exit_code == 0


@pytest.mark.asyncio
async def test_chain_single_step(monkeypatch):
    """Chain with a single step behaves identically to run_script."""
    from bvd import config
    monkeypatch.setattr(config.settings, "enable_chains", True)

    long_prompt = "a" * 500
    step = _make_step("step-a", "print('result')")
    chain = _make_chain(["step-a"])

    result = await run_chain(chain, long_prompt, [step])

    assert result.output == "result"
    assert result.exit_code == 0
    assert len(result.step_outputs) == 1


@pytest.mark.asyncio
async def test_chain_pending_dep(monkeypatch):
    """A pending dep skill aborts the chain (not executed)."""
    from bvd import config
    monkeypatch.setattr(config.settings, "enable_chains", True)

    prompt = "original prompt"
    step_a = _make_step("step-a", "print('ok')")
    step_b = _make_step("step-b", "print('pending dep')")
    step_b.status = "pending"
    chain = _make_chain(["step-a", "step-b"])

    result = await run_chain(chain, prompt, [step_a, step_b])

    assert result.output == prompt
    assert result.failed_step == 1


@pytest.mark.asyncio
async def test_chain_with_finalizer(monkeypatch):
    """Chain with a finalizer (chain_script.code) runs it after all steps."""
    from bvd import config
    monkeypatch.setattr(config.settings, "enable_chains", True)

    long_prompt = "a" * 500
    step = _make_step("step-a", "print('condensed')")
    # Finalizer: chain script has its own code that wraps the last step's output
    chain = _make_chain(["step-a"], finalizer_code="print(f'[done] {PROMPT}')")

    result = await run_chain(chain, long_prompt, [step])

    assert result.output == "[done] condensed"
    assert result.exit_code == 0
    assert len(result.step_outputs) == 2  # step-a + finalizer


@pytest.mark.asyncio
async def test_chain_import_cycle_detection(tmp_path, monkeypatch):
    """Installing A→B→A raises ValueError at import time."""
    from unittest.mock import patch

    from bvd.engine.library import Library

    with patch("bvd.config.settings.db_path", tmp_path / "cycle_test.db"):
        lib = Library()
        await lib.init()

        # Install skill A (standalone, no chain)
        skill_a_json = {
            "id": "skill-a", "name": "A", "description": "A",
            "bss_version": "1.0",
        }
        await lib.import_bss(skill_a_json, "print(PROMPT)")

        # Install skill B with chain → A (valid, no cycle yet)
        skill_b_json = {
            "id": "skill-b", "name": "B", "description": "B",
            "bss_version": "1.1", "chain": ["skill-a"],
        }
        await lib.import_bss(skill_b_json, "")

        # Now try to update skill A to chain → B (would create A→B→A cycle)
        skill_a_cycle_json = {
            "id": "skill-a", "name": "A cyclic", "description": "A",
            "bss_version": "1.1", "chain": ["skill-b"],
        }
        with pytest.raises(ValueError, match="[Cc]ircular"):
            await lib.import_bss(skill_a_cycle_json, "")

        await lib.close()
