import json
import re
from types import SimpleNamespace

import pytest

from abi.errors import ToolError
from abi.exporters import NextflowExporter
from abi.plugins import get_plugin
from abi.schemas import PlanStep
from abi.tools import ToolRegistry


def test_nextflow_exporter_renders_processes_from_abi_plan(tmp_path):
    plugin = get_plugin("metatranscriptomics")
    config = plugin.load_config(
        overrides={
            "outdir": str(tmp_path / "results"),
            "log_dir": str(tmp_path / "log"),
        }
    )
    plan = plugin.build_plan(config)

    script = NextflowExporter().export(plan, config, plugin.registry())

    assert "nextflow.enable.dsl=2" in script
    assert "process RNA1_QC_FASTP" in script
    assert "process RNA1_ALIGNMENT_STAR" in script
    assert "process RNA1_EXPRESSION_FEATURECOUNTS" in script
    assert "fastp -i" in script
    assert "/examples/fixtures/tiny_R1.fastq" in script
    assert "STAR --runThreadN 4" in script
    assert 'featureCounts -T "$1" -p' in script
    assert "retrying featureCounts without -p" in script
    assert "workflow {" in script
    assert "abi_root = Channel.value('abi_nextflow_start')" in script
    assert "ch_RNA1_QC_FASTP = RNA1_QC_FASTP(abi_root)" in script
    assert "ch_RNA1_ALIGNMENT_STAR = RNA1_ALIGNMENT_STAR(ch_RNA1_QC_FASTP)" in script
    assert (
        "ch_RNA1_EXPRESSION_FEATURECOUNTS = RNA1_EXPRESSION_FEATURECOUNTS(ch_RNA1_ALIGNMENT_STAR)"
    ) in script


def test_nextflow_exporter_smoke_mode_does_not_call_real_tools(tmp_path):
    plugin = get_plugin("metatranscriptomics")
    config = plugin.load_config(overrides={"outdir": str(tmp_path / "results")})
    plan = plugin.build_plan(config)

    script = NextflowExporter().export(plan, config, plugin.registry(), smoke=True)

    assert "// Export mode: smoke" in script
    assert "ABI Nextflow smoke step" in script
    assert "fastp -i" not in script
    assert "STAR --runThreadN" not in script
    assert "featureCounts -T" not in script


def test_nextflow_exporter_writes_script(tmp_path):
    plugin = get_plugin("metatranscriptomics")
    config = plugin.load_config(overrides={"outdir": str(tmp_path / "results")})
    plan = plugin.build_plan(config)
    output = tmp_path / "workflow.nf"

    path = NextflowExporter().write(plan, config, plugin.registry(), output)

    assert path == output
    assert output.exists()
    assert output.read_text(encoding="utf-8").startswith("// Generated by ABI NextflowExporter.")


def test_nextflow_exporter_overwrites_existing_script_after_successful_export(tmp_path):
    plugin = get_plugin("metatranscriptomics")
    config = plugin.load_config(overrides={"outdir": str(tmp_path / "results")})
    plan = plugin.build_plan(config)
    output = tmp_path / "workflow.nf"
    output.write_text("old workflow\n", encoding="utf-8")

    NextflowExporter().write(plan, config, plugin.registry(), output)

    assert output.read_text(encoding="utf-8").startswith("// Generated by ABI NextflowExporter.")


def test_nextflow_exporter_preserves_existing_script_when_export_fails(tmp_path):
    output = tmp_path / "workflow.nf"
    output.write_text("old workflow\n", encoding="utf-8")
    bad_step = PlanStep("bad", "Bad", "missing", "test")
    plan = SimpleNamespace(analysis_type="test", project_name="test", threads=1, steps=[bad_step])

    with pytest.raises(ToolError, match="unknown tool"):
        NextflowExporter().write(plan, {}, ToolRegistry([]), output)

    assert output.read_text(encoding="utf-8") == "old workflow\n"


def test_worker_internal_process_is_invoked_and_payload_matches_step_runner_contract(
    tmp_path, monkeypatch
):
    step = PlanStep(
        "normalize",
        "Normalize",
        "internal",
        "normalization",
        inputs={"source": "input.tsv"},
        outputs={"output_dir": str(tmp_path / "normalized")},
        params={
            "threshold": 2,
            "_internal_handler": {
                "handler_id": "normalize",
                "execution_scope": "worker",
            },
        },
    )
    plan = SimpleNamespace(
        analysis_type="example_plugin", project_name="test", threads=1, steps=[step]
    )
    config = {"outdir": str(tmp_path / "results"), "feature": {"enabled": True}}

    script = NextflowExporter().export(plan, config, ToolRegistry([]), project_root=tmp_path)

    assert "ch_NORMALIZE = NORMALIZE(abi_root)" in script
    assert '"plugin_id": "example_plugin"' in script
    assert '"step": {' in script
    assert '"step_name": "Normalize"' in script
    assert '"config": {' in script
    assert '"provenance_dir":' in script
    assert '"result_path": ".abi_result.json"' in script
    assert "chmod 600 .abi_payload.json" in script
    assert "path '.abi_result.json'" in script

    payload_match = re.search(
        r"<<'ABI_PAYLOAD'\n(?P<payload>.*?)\nABI_PAYLOAD", script, flags=re.DOTALL
    )
    assert payload_match is not None
    payload = json.loads(payload_match.group("payload"))
    assert set(payload) == {
        "plugin_id",
        "step",
        "config",
        "provenance_dir",
        "result_path",
    }

    from abi import step_runner

    monkeypatch.chdir(tmp_path)
    monkeypatch.setattr(step_runner, "get_plugin", lambda plugin_id: object())
    monkeypatch.setattr(
        step_runner,
        "execute_step",
        lambda plugin, actual_step, actual_config, provenance_dir: step_runner.StepExecutionResult(
            step_id=actual_step.step_id,
            tool_id=actual_step.tool_id,
            status="success",
        ),
    )
    payload_path = tmp_path / ".abi_payload.json"
    payload_path.write_text(json.dumps(payload), encoding="utf-8")

    result = step_runner.execute_step_payload(payload_path)

    assert result.status == "success"
    assert (tmp_path / ".abi_result.json").exists()


def test_driver_internal_step_fails_fast_even_without_upstream_dependencies(tmp_path):
    step = PlanStep(
        "prepare",
        "Prepare",
        "internal",
        "preflight",
        params={
            "_internal_handler": {
                "handler_id": "prepare_inputs",
                "execution_scope": "driver",
            }
        },
    )
    plan = SimpleNamespace(
        analysis_type="example_plugin", project_name="test", threads=1, steps=[step]
    )

    with pytest.raises(ToolError, match=r"prepare.*prepare_inputs"):
        NextflowExporter().export(plan, {"outdir": str(tmp_path)}, ToolRegistry([]))


def test_nextflow_exporter_serializes_metagenomic_plasmid_steps_without_path_edges(tmp_path):
    """C09: worker-scoped internal handlers now generate Nextflow processes.

    Metagenomic plasmid DAG has worker-scoped internal handlers that previously
    blocked export.  C09 generates Nextflow processes for them via
    ``abi run-step``."""
    plugin = get_plugin("metagenomic_plasmid")
    config = plugin.load_config(
        "examples/config_minimal.yaml",
        overrides={"outdir": str(tmp_path / "plasmid"), "log_dir": str(tmp_path / "log")},
    )
    plan = plugin.build_plan(config)

    # Export must succeed — no longer blocked by internal worker steps.
    script = NextflowExporter().export(plan, config, plugin.registry())

    # The generated script should contain process definitions.
    assert "process " in script

    # Internal worker processes should invoke abi run-step.
    assert "abi run-step" in script or "abi-nextflow-smoke" in script
