//! DAG construction and layout for the TUI.
//!
//! Builds a `petgraph` directed graph from a resolved [`crate::model::Pipeline`],
//! then computes a layered layout: stage columns, job rows, and edge routing.
//! The [`GraphLayout`] result is what the [`crate::ui`] module draws.

mod layout;

pub use layout::{DepKind, Edge, GraphLayout, JobNode, StageHeader};

use indexmap::IndexMap;
use petgraph::graph::{DiGraph, NodeIndex};

use crate::model::Pipeline;

/// Build a directed graph from the resolved pipeline.
pub fn build_graph(pipeline: &Pipeline) -> (DiGraph<String, DepKind>, IndexMap<String, NodeIndex>) {
    let mut graph = DiGraph::new();
    let mut node_indices: IndexMap<String, NodeIndex> = IndexMap::new();

    // Add nodes
    for (name, _) in &pipeline.jobs {
        let idx = graph.add_node(name.clone());
        node_indices.insert(name.clone(), idx);
    }

    // Add edges
    for (name, job) in &pipeline.jobs {
        let target_idx = node_indices[name];

        if !job.needs.is_empty() {
            // Explicit needs: add edges from each needed job
            for need in &job.needs {
                if let Some(&source_idx) = node_indices.get(need.job_name.as_str()) {
                    graph.add_edge(source_idx, target_idx, DepKind::Needs);
                }
            }
        } else {
            // Implicit stage dependencies: depends on all jobs in previous stage
            if let Some(stage_idx) = pipeline.stage_index(&job.stage)
                && stage_idx > 0
            {
                let prev_stage = &pipeline.stages[stage_idx - 1];
                for prev_job_name in &prev_stage.job_names {
                    if let Some(&source_idx) = node_indices.get(prev_job_name) {
                        graph.add_edge(source_idx, target_idx, DepKind::Stage);
                    }
                }
            }
        }
    }

    (graph, node_indices)
}

/// Compute the layout for terminal rendering.
pub fn compute_layout(pipeline: &Pipeline) -> GraphLayout {
    let (graph, node_indices) = build_graph(pipeline);
    GraphLayout::compute(pipeline, &graph, &node_indices)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::model::{Job, JobDebugInfo, ResolvedNeed, Stage};
    use petgraph::visit::EdgeRef;

    fn make_job(name: &str, stage: &str) -> Job {
        Job {
            name: name.to_string(),
            stage: stage.to_string(),
            image: None,
            services: vec![],
            variables: IndexMap::new(),
            before_script: vec![],
            script: vec!["echo hi".to_string()],
            after_script: vec![],
            needs: vec![],
            artifact_sources: Some(vec![]),
            tags: vec![],
            when_val: "on_success".to_string(),
            allow_failure: false,
            rules_never: false,
            unresolved_extends: vec![],
            rules_summary: vec![],
            artifacts_paths: vec![],
            artifacts_exclude: vec![],
            artifacts_untracked: false,
            artifacts_when: "on_success".to_string(),
            dotenv_report: None,
            timeout: None,
            resource_group: None,
            coverage: None,
            resolved_yaml: String::new(),
            trigger_file: None,
            build_context: None,
            docker_entrypoint: None,
            step_id: None,
            steps: vec![],
            debug_info: JobDebugInfo::default(),
        }
    }

    fn make_job_with_needs(name: &str, stage: &str, needs: &[&str]) -> Job {
        let mut job = make_job(name, stage);
        job.needs = needs
            .iter()
            .map(|n| ResolvedNeed {
                job_name: n.to_string(),
                artifacts: true,
                optional: false,
            })
            .collect();
        job
    }

    fn pipeline_from_jobs(stages: &[(&str, Vec<Job>)]) -> Pipeline {
        let mut jobs = IndexMap::new();
        let mut stage_list = Vec::new();
        for (stage_name, stage_jobs) in stages {
            let job_names: Vec<String> = stage_jobs.iter().map(|j| j.name.clone()).collect();
            stage_list.push(Stage {
                name: stage_name.to_string(),
                job_names,
            });
            for job in stage_jobs {
                jobs.insert(job.name.clone(), job.clone());
            }
        }
        Pipeline {
            stages: stage_list,
            jobs,
            execution: Default::default(),
            checkout_path: None,
        }
    }

    /// Collect edges as (from_name, to_name, kind) tuples.
    fn edge_list(graph: &DiGraph<String, DepKind>) -> Vec<(String, String, DepKind)> {
        graph
            .edge_references()
            .map(|e| {
                (
                    graph[e.source()].clone(),
                    graph[e.target()].clone(),
                    *e.weight(),
                )
            })
            .collect()
    }

    // --- Node creation ---

    #[test]
    fn test_build_graph_nodes_match_jobs() {
        let p = pipeline_from_jobs(&[
            (
                "build",
                vec![make_job("a", "build"), make_job("b", "build")],
            ),
            ("test", vec![make_job("c", "test")]),
        ]);
        let (graph, indices) = build_graph(&p);

        assert_eq!(graph.node_count(), 3);
        assert_eq!(indices.len(), 3);
        for name in &["a", "b", "c"] {
            assert!(indices.contains_key(*name));
        }
    }

    #[test]
    fn test_build_graph_empty_pipeline() {
        let p = Pipeline {
            stages: vec![],
            jobs: IndexMap::new(),
            execution: Default::default(),
            checkout_path: None,
        };
        let (graph, indices) = build_graph(&p);

        assert_eq!(graph.node_count(), 0);
        assert_eq!(graph.edge_count(), 0);
        assert!(indices.is_empty());
    }

    // --- Explicit needs edges ---

    #[test]
    fn test_explicit_needs_creates_needs_edge() {
        let p = pipeline_from_jobs(&[
            ("build", vec![make_job("compile", "build")]),
            (
                "test",
                vec![make_job_with_needs("unit", "test", &["compile"])],
            ),
        ]);
        let (graph, _) = build_graph(&p);

        let edges = edge_list(&graph);
        assert_eq!(edges.len(), 1);
        assert_eq!(edges[0].0, "compile");
        assert_eq!(edges[0].1, "unit");
        assert_eq!(edges[0].2, DepKind::Needs);
    }

    #[test]
    fn test_explicit_needs_suppresses_stage_edges() {
        // If a job has explicit needs, it should NOT get implicit stage edges
        let p = pipeline_from_jobs(&[
            (
                "build",
                vec![make_job("a", "build"), make_job("b", "build")],
            ),
            (
                "test",
                vec![make_job_with_needs("c", "test", &["a"])], // only needs a, not b
            ),
        ]);
        let (graph, _) = build_graph(&p);

        let c_incoming: Vec<_> = edge_list(&graph)
            .into_iter()
            .filter(|(_, to, _)| to == "c")
            .collect();
        assert_eq!(c_incoming.len(), 1);
        assert_eq!(c_incoming[0].0, "a");
        assert_eq!(c_incoming[0].2, DepKind::Needs);
    }

    #[test]
    fn test_missing_needed_job_is_skipped() {
        // needs a job that doesn't exist → no edge, no panic
        let p = pipeline_from_jobs(&[(
            "test",
            vec![make_job_with_needs("unit", "test", &["nonexistent"])],
        )]);
        let (graph, _) = build_graph(&p);

        assert_eq!(graph.edge_count(), 0);
    }

    // --- Implicit stage edges ---

    #[test]
    fn test_implicit_stage_edges() {
        // No explicit needs → depends on all jobs in previous stage
        let p = pipeline_from_jobs(&[
            (
                "build",
                vec![make_job("a", "build"), make_job("b", "build")],
            ),
            ("test", vec![make_job("c", "test")]),
        ]);
        let (graph, _) = build_graph(&p);

        let edges = edge_list(&graph);
        assert_eq!(edges.len(), 2);

        let mut sources: Vec<String> = edges.iter().map(|(from, _, _)| from.clone()).collect();
        sources.sort();
        assert_eq!(sources, vec!["a", "b"]);
        assert!(edges.iter().all(|(_, to, _)| to == "c"));
        assert!(edges.iter().all(|(_, _, kind)| *kind == DepKind::Stage));
    }

    #[test]
    fn test_first_stage_no_edges() {
        // Jobs in the first stage have no predecessors
        let p = pipeline_from_jobs(&[
            (
                "build",
                vec![make_job("a", "build"), make_job("b", "build")],
            ),
            ("test", vec![make_job("c", "test")]),
        ]);
        let (graph, _) = build_graph(&p);

        let a_incoming: Vec<_> = edge_list(&graph)
            .into_iter()
            .filter(|(_, to, _)| to == "a")
            .collect();
        let b_incoming: Vec<_> = edge_list(&graph)
            .into_iter()
            .filter(|(_, to, _)| to == "b")
            .collect();
        assert!(a_incoming.is_empty());
        assert!(b_incoming.is_empty());
    }

    #[test]
    fn test_implicit_deps_chain_three_stages() {
        // build → test → deploy, all implicit
        let p = pipeline_from_jobs(&[
            ("build", vec![make_job("a", "build")]),
            ("test", vec![make_job("b", "test")]),
            ("deploy", vec![make_job("c", "deploy")]),
        ]);
        let (graph, _) = build_graph(&p);

        let edges = edge_list(&graph);
        assert_eq!(edges.len(), 2);
        assert!(edges.iter().any(|(f, t, _)| f == "a" && t == "b"));
        assert!(edges.iter().any(|(f, t, _)| f == "b" && t == "c"));
    }

    // --- Mixed explicit + implicit ---

    #[test]
    fn test_mixed_needs_and_implicit() {
        // build: a, b
        // test: c (needs a explicitly), d (no needs → implicit from a, b)
        let p = pipeline_from_jobs(&[
            (
                "build",
                vec![make_job("a", "build"), make_job("b", "build")],
            ),
            (
                "test",
                vec![
                    make_job_with_needs("c", "test", &["a"]),
                    make_job("d", "test"),
                ],
            ),
        ]);
        let (graph, _) = build_graph(&p);

        let c_edges: Vec<_> = edge_list(&graph)
            .into_iter()
            .filter(|(_, to, _)| to == "c")
            .collect();
        assert_eq!(c_edges.len(), 1);
        assert_eq!(c_edges[0].2, DepKind::Needs);

        let d_edges: Vec<_> = edge_list(&graph)
            .into_iter()
            .filter(|(_, to, _)| to == "d")
            .collect();
        assert_eq!(d_edges.len(), 2);
        assert!(d_edges.iter().all(|(_, _, kind)| *kind == DepKind::Stage));
    }

    // --- Edge kind preservation ---

    #[test]
    fn test_edge_kind_needs_vs_stage() {
        let p = pipeline_from_jobs(&[
            ("build", vec![make_job("a", "build")]),
            (
                "test",
                vec![
                    make_job_with_needs("explicit", "test", &["a"]),
                    make_job("implicit", "test"),
                ],
            ),
        ]);
        let (graph, _) = build_graph(&p);

        let edges = edge_list(&graph);
        let explicit_edge = edges.iter().find(|(_, to, _)| to == "explicit").unwrap();
        let implicit_edge = edges.iter().find(|(_, to, _)| to == "implicit").unwrap();

        assert_eq!(explicit_edge.2, DepKind::Needs);
        assert_eq!(implicit_edge.2, DepKind::Stage);
    }

    // --- Single job, single stage ---

    #[test]
    fn test_single_job_no_edges() {
        let p = pipeline_from_jobs(&[("build", vec![make_job("a", "build")])]);
        let (graph, _) = build_graph(&p);

        assert_eq!(graph.node_count(), 1);
        assert_eq!(graph.edge_count(), 0);
    }
}
