import pytest

from abi.contracts import WorkflowSpec, WorkflowStepSpec, load_workflow_spec
from abi.dag import infer_dag, process_name
from abi.plugins import get_plugin
from abi.schemas import ABIError, ABIPlanStep


def test_infer_dag_links_steps_by_declared_paths(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)

    dag = infer_dag(plan.steps)

    assert dag.roots == ["RNA1_qc_fastp"]
    assert dag.edges["RNA1_qc_fastp"] == []
    assert dag.edges["RNA1_alignment_star"] == ["RNA1_qc_fastp"]
    assert dag.edges["RNA1_expression_featurecounts"] == ["RNA1_alignment_star"]
    assert dag.topological_order == [
        "RNA1_qc_fastp",
        "RNA1_alignment_star",
        "RNA1_expression_featurecounts",
    ]


def test_infer_dag_rejects_duplicate_output_paths(tmp_path):
    steps = [
        ABIPlanStep(
            step_id="a",
            sample_id="S1",
            step_name="first",
            tool_id="mock",
            category="test",
            outputs={"out": str(tmp_path / "shared.txt")},
        ),
        ABIPlanStep(
            step_id="b",
            sample_id="S1",
            step_name="second",
            tool_id="mock",
            category="test",
            outputs={"out": str(tmp_path / "shared.txt")},
        ),
    ]

    with pytest.raises(ABIError, match="Duplicate ABI output path"):
        infer_dag(steps)


def test_infer_dag_allows_shared_step_output_directories(tmp_path):
    shared = tmp_path / "01_qc" / "S1"
    steps = [
        ABIPlanStep(
            step_id="fastp",
            sample_id="S1",
            step_name="qc",
            tool_id="fastp",
            category="qc",
            outputs={"output_dir": str(shared)},
        ),
        ABIPlanStep(
            step_id="fastqc",
            sample_id="S1",
            step_name="qc",
            tool_id="fastqc",
            category="qc",
            outputs={"output_dir": str(shared)},
        ),
    ]

    dag = infer_dag(steps)

    assert dag.edges == {"fastp": [], "fastqc": []}
    assert dag.binding_for("fastp").produced_paths["output_dir"] == str(shared)
    assert dag.binding_for("fastqc").produced_paths["output_dir"] == str(shared)


def test_extensionless_output_uses_declared_contract_type(tmp_path):
    shared = str(tmp_path / "artifact")
    file_contract = {"_contract": {"outputs": {"artifact": {"type": "file"}}}}
    steps = [
        ABIPlanStep(
            step_id=step_id,
            sample_id="S1",
            step_name="test",
            tool_id="mock",
            category="test",
            outputs={"artifact": shared},
            params=file_contract,
        )
        for step_id in ("a", "b")
    ]

    with pytest.raises(ABIError, match="Duplicate ABI output path"):
        infer_dag(steps)


def test_infer_dag_sequential_fallback_links_unresolved_steps(tmp_path):
    shared = tmp_path / "01_qc" / "S1"
    steps = [
        ABIPlanStep(
            step_id="fastp",
            sample_id="S1",
            step_name="qc",
            tool_id="fastp",
            category="qc",
            outputs={"output_dir": str(shared)},
        ),
        ABIPlanStep(
            step_id="fastqc",
            sample_id="S1",
            step_name="qc",
            tool_id="fastqc",
            category="qc",
            outputs={"output_dir": str(shared)},
        ),
    ]

    dag = infer_dag(steps, sequential_fallback=True)

    assert dag.roots == ["fastp"]
    assert dag.edges["fastqc"] == ["fastp"]


def test_infer_dag_accepts_metagenomic_plasmid_shared_output_dirs(tmp_path):
    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)

    dag = infer_dag(plan.steps, sequential_fallback=True)

    # The DAG-driven planner generates steps from pipeline_dag.yaml.
    # The exact root depends on which optional stages are enabled in
    # the minimal config — it could be fastqc_raw, host_prediction,
    # assembly, or fastp.  Verify the DAG is structurally sound
    # regardless.
    # DAG 驱动的规划器从 pipeline_dag.yaml 生成步骤。
    # 根节点取决于最小配置中启用的可选阶段 —— 可能是 fastqc_raw、
    # host_prediction、assembly 或 fastp。验证 DAG 结构正确即可。
    assert len(dag.roots) >= 1, f"Expected at least 1 root, got {dag.roots}"
    assert "S1_qc_fastp" in dag.topological_order
    # S2 is assembly-only in examples/sample_sheet.tsv, so the planner must
    # preserve its own platform route instead of forcing the first sample's
    # Illumina route onto the whole project.
    assert "S2_assembly_internal" in dag.topological_order
    assert "S2_qc_fastp" not in dag.topological_order
    # Verify the plasmid detection chain is intact
    assert "S1_plasmid_detection_genomad" in dag.topological_order
    assert "S1_plasmid_consensus_internal" in dag.topological_order


def test_infer_dag_ignores_structured_non_path_values():
    step = ABIPlanStep(
        step_id="a",
        sample_id="S1",
        step_name="first",
        tool_id="mock",
        category="test",
        inputs={"metadata": {"sample": "S1"}},
        outputs={"records": ["not", "a", "path"]},
    )

    dag = infer_dag([step])
    binding = dag.binding_for("a")

    assert binding.consumed_paths == {}
    assert binding.produced_paths == {}


def test_infer_dag_links_list_valued_aggregate_inputs(tmp_path):
    first = tmp_path / "S1.counts.tsv"
    second = tmp_path / "S2.counts.tsv"
    steps = [
        ABIPlanStep(
            step_id="S1_counts",
            sample_id="S1",
            step_name="counts",
            tool_id="featurecounts",
            category="expression",
            outputs={"counts": str(first)},
        ),
        ABIPlanStep(
            step_id="S2_counts",
            sample_id="S2",
            step_name="counts",
            tool_id="featurecounts",
            category="expression",
            outputs={"counts": str(second)},
        ),
        ABIPlanStep(
            step_id="matrix",
            sample_id=None,
            step_name="matrix",
            tool_id="build_count_matrix",
            category="expression",
            inputs={"count_files": [str(first), str(second)]},
        ),
    ]

    dag = infer_dag(steps)

    assert dag.edges["matrix"] == ["S1_counts", "S2_counts"]
    assert dag.binding_for("matrix").consumed_paths == {
        "count_files[0]": str(first),
        "count_files[1]": str(second),
    }


def test_process_name_is_nextflow_safe():
    assert process_name("1 sample/qc-fastp") == "STEP_1_SAMPLE_QC_FASTP"


def test_metatranscriptomics_declared_workflow_is_golden_dag(tmp_path):
    plugin = get_plugin("metatranscriptomics")
    config = plugin.load_config(
        overrides={"outdir": str(tmp_path / "results"), "log_dir": str(tmp_path / "logs")}
    )
    plan = plugin.build_plan(config)
    workflow = load_workflow_spec(plugin.root)

    dag = infer_dag(plan.steps, workflow_spec=workflow)

    assert dag.edges["RNA1_alignment_star"] == ["RNA1_qc_fastp"]
    assert dag.edges["RNA1_expression_featurecounts"] == ["RNA1_alignment_star"]


def test_declared_workflow_binds_duplicate_tools_per_sample():
    steps = []
    for sample_id in ("S1", "S2"):
        steps.extend(
            [
                ABIPlanStep(
                    step_id=f"{sample_id}_qc",
                    sample_id=sample_id,
                    step_name="qc",
                    tool_id="fastp",
                    category="qc",
                ),
                ABIPlanStep(
                    step_id=f"{sample_id}_align",
                    sample_id=sample_id,
                    step_name="align",
                    tool_id="star",
                    category="alignment",
                ),
            ]
        )
    workflow = WorkflowSpec(
        name="multi-sample",
        steps=[
            WorkflowStepSpec(id="qc", tool="fastp", after=[]),
            WorkflowStepSpec(id="align", tool="star", after=["qc"]),
        ],
    )

    dag = infer_dag(steps, workflow_spec=workflow)

    assert dag.edges["S1_align"] == ["S1_qc"]
    assert dag.edges["S2_align"] == ["S2_qc"]


def test_metagenomic_plasmid_core_declared_workflow_is_golden_dag():
    plugin = get_plugin("metagenomic_plasmid")
    config = plugin.load_config("examples/metagenomic_plasmid/config_core_demo.yaml")
    plan = plugin.build_plan(config)
    workflow = load_workflow_spec(plugin.root)

    dag = infer_dag(plan.steps, workflow_spec=workflow)
    positions = {step_id: index for index, step_id in enumerate(dag.topological_order)}

    assert positions["S1_qc_fastp"] < positions["S1_assembly_megahit"]
    assert positions["S1_assembly_megahit"] < positions["S1_plasmid_detection_genomad"]
    assert positions["S1_plasmid_detection_genomad"] < positions["S1_annotation_bakta"]
    assert positions["S1_plasmid_detection_genomad"] < positions["S1_typing_plasmidfinder"]
    assert positions["S1_plasmid_detection_genomad"] < positions["S1_abundance_coverm"]
    assert positions["S1_abundance_coverm"] < positions["report_markdown"]


def test_declared_workflow_path_mismatch_emits_warning(caplog):
    steps = [
        ABIPlanStep(
            step_id="qc",
            sample_id="S1",
            step_name="qc",
            tool_id="fastp",
            category="qc",
            outputs={"reads": "trimmed.fastq.gz"},
        ),
        ABIPlanStep(
            step_id="alignment",
            sample_id="S1",
            step_name="alignment",
            tool_id="star",
            category="alignment",
            inputs={"reads": "different.fastq.gz"},
        ),
    ]
    workflow = WorkflowSpec(
        name="mismatch",
        steps=[
            WorkflowStepSpec(id="qc", tool="fastp", after=[]),
            WorkflowStepSpec(id="alignment", tool="star", after=["qc"]),
        ],
    )

    infer_dag(steps, workflow_spec=workflow)

    assert "declared-but-not-inferred=['qc']" in caplog.text


def test_declared_workflow_path_mismatch_can_fail_strict_validation():
    steps = [
        ABIPlanStep(
            step_id="qc",
            sample_id="S1",
            step_name="qc",
            tool_id="fastp",
            category="qc",
            outputs={"reads": "trimmed.fastq.gz"},
        ),
        ABIPlanStep(
            step_id="alignment",
            sample_id="S1",
            step_name="alignment",
            tool_id="star",
            category="alignment",
            inputs={"reads": "different.fastq.gz"},
        ),
    ]
    workflow = WorkflowSpec(
        name="mismatch",
        steps=[
            WorkflowStepSpec(id="qc", tool="fastp", after=[]),
            WorkflowStepSpec(id="alignment", tool="star", after=["qc"]),
        ],
    )

    with pytest.raises(ABIError, match="DAG mismatch"):
        infer_dag(steps, workflow_spec=workflow, strict_workflow=True)


def test_declared_workflow_missing_node_fails():
    step = ABIPlanStep(
        step_id="qc",
        sample_id="S1",
        step_name="qc",
        tool_id="fastp",
        category="qc",
    )
    workflow = WorkflowSpec(
        name="broken",
        steps=[WorkflowStepSpec(id="qc", tool="fastp", after=["undefined"])],
    )

    with pytest.raises(ABIError, match="undefined dependencies"):
        infer_dag([step], workflow_spec=workflow)


def test_declared_workflow_cycle_fails():
    steps = [
        ABIPlanStep(step_id="a", sample_id=None, step_name="a", tool_id="a", category="test"),
        ABIPlanStep(step_id="b", sample_id=None, step_name="b", tool_id="b", category="test"),
        ABIPlanStep(step_id="c", sample_id=None, step_name="c", tool_id="c", category="test"),
    ]
    workflow = WorkflowSpec(
        name="cycle",
        steps=[
            WorkflowStepSpec(id="a", tool="a", after=["c"]),
            WorkflowStepSpec(id="b", tool="b", after=["a"]),
            WorkflowStepSpec(id="c", tool="c", after=["b"]),
        ],
    )

    with pytest.raises(ABIError, match="Cycle detected"):
        infer_dag(steps, workflow_spec=workflow)
