pub mod types;

use crate::parse::{SpecKind, SpecNode};
use types::{DiffKind, DiffNode, FileDiff};

pub fn diff_spec_nodes(base: &[SpecNode], head: &[SpecNode]) -> Vec<DiffNode> {
    let mut results = Vec::new();
    let mut matched_head: Vec<bool> = vec![false; head.len()];

    for base_node in base {
        if let Some((idx, head_node)) = find_match(base_node, head, &matched_head) {
            matched_head[idx] = true;
            let child_diff = if base_node.kind == SpecKind::Group {
                diff_spec_nodes(&base_node.children, &head_node.children)
            } else {
                vec![]
            };

            let has_changes = child_diff.iter().any(|d| d.kind != DiffKind::Unchanged);

            results.push(DiffNode {
                name: head_node.name.clone(),
                kind: if has_changes { DiffKind::Modified } else { DiffKind::Unchanged },
                old_name: None,
                param_cases: head_node.parameterized.as_ref().map(|p| p.case_count),
                children: child_diff,
            });
        } else if let Some((idx, head_node)) = find_rename_candidate(base_node, head, &matched_head) {
            matched_head[idx] = true;
            let child_diff = if base_node.kind == SpecKind::Group {
                diff_spec_nodes(&base_node.children, &head_node.children)
            } else {
                vec![]
            };
            results.push(DiffNode {
                name: head_node.name.clone(),
                kind: DiffKind::Renamed,
                old_name: Some(base_node.name.clone()),
                param_cases: head_node.parameterized.as_ref().map(|p| p.case_count),
                children: child_diff,
            });
        } else {
            results.push(DiffNode {
                name: base_node.name.clone(),
                kind: DiffKind::Removed,
                old_name: None,
                param_cases: base_node.parameterized.as_ref().map(|p| p.case_count),
                children: vec![],
            });
        }
    }

    for (idx, head_node) in head.iter().enumerate() {
        if !matched_head[idx] {
            results.push(DiffNode {
                name: head_node.name.clone(),
                kind: DiffKind::Added,
                old_name: None,
                param_cases: head_node.parameterized.as_ref().map(|p| p.case_count),
                children: if head_node.kind == SpecKind::Group {
                    head_node.children.iter().map(make_added).collect()
                } else {
                    vec![]
                },
            });
        }
    }

    results
}

fn find_match<'a>(
    base_node: &SpecNode,
    head: &'a [SpecNode],
    matched: &[bool],
) -> Option<(usize, &'a SpecNode)> {
    for (idx, head_node) in head.iter().enumerate() {
        if !matched[idx] && base_node.name == head_node.name && base_node.kind == head_node.kind {
            return Some((idx, head_node));
        }
    }
    None
}

fn find_rename_candidate<'a>(
    base_node: &SpecNode,
    head: &'a [SpecNode],
    matched: &[bool],
) -> Option<(usize, &'a SpecNode)> {
    if base_node.kind != SpecKind::Spec {
        return None;
    }

    let mut best: Option<(usize, &SpecNode, f64)> = None;

    for (idx, head_node) in head.iter().enumerate() {
        if matched[idx] || head_node.kind != base_node.kind {
            continue;
        }
        let sim = name_similarity(&base_node.name, &head_node.name);
        if sim >= 0.5
            && best.as_ref().is_none_or(|(_, _, best_sim)| sim > *best_sim)
        {
            best = Some((idx, head_node, sim));
        }
    }

    best.map(|(idx, node, _)| (idx, node))
}

fn name_similarity(a: &str, b: &str) -> f64 {
    if a == b {
        return 1.0;
    }
    if a.is_empty() || b.is_empty() {
        return 0.0;
    }

    let a_words: Vec<&str> = a.split_whitespace().collect();
    let b_words: Vec<&str> = b.split_whitespace().collect();

    let common = a_words.iter().filter(|w| b_words.contains(w)).count();
    let total = a_words.len().max(b_words.len());

    if total == 0 {
        return 0.0;
    }

    common as f64 / total as f64
}

pub fn filter_file_diffs(file_diffs: Vec<FileDiff>, pattern: &str) -> Vec<FileDiff> {
    let pattern_lower = pattern.to_lowercase();
    file_diffs
        .into_iter()
        .filter_map(|mut fd| {
            if fd.path.to_lowercase().contains(&pattern_lower) {
                return Some(fd);
            }
            fd.nodes = filter_nodes(fd.nodes, &pattern_lower);
            if fd.nodes.is_empty() {
                None
            } else {
                Some(fd)
            }
        })
        .collect()
}

fn filter_nodes(nodes: Vec<DiffNode>, pattern: &str) -> Vec<DiffNode> {
    nodes
        .into_iter()
        .filter_map(|mut node| {
            if node.name.to_lowercase().contains(pattern) {
                return Some(node);
            }
            node.children = filter_nodes(node.children, pattern);
            if node.children.is_empty() {
                None
            } else {
                Some(node)
            }
        })
        .collect()
}

fn make_added(node: &SpecNode) -> DiffNode {
    DiffNode {
        name: node.name.clone(),
        kind: DiffKind::Added,
        old_name: None,
        param_cases: node.parameterized.as_ref().map(|p| p.case_count),
        children: node.children.iter().map(make_added).collect(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::parse::SpecNode;

    #[test]
    fn diff_identical_trees() {
        let nodes = vec![
            SpecNode::spec("validates email", 1),
            SpecNode::spec("requires password", 2),
        ];
        let diff = diff_spec_nodes(&nodes, &nodes);
        assert_eq!(diff.len(), 2);
        assert!(diff.iter().all(|d| d.kind == DiffKind::Unchanged));
    }

    #[test]
    fn diff_added_spec() {
        let base = vec![SpecNode::spec("validates email", 1)];
        let head = vec![
            SpecNode::spec("validates email", 1),
            SpecNode::spec("validates uniqueness", 2),
        ];
        let diff = diff_spec_nodes(&base, &head);
        assert_eq!(diff.len(), 2);
        assert_eq!(diff[0].kind, DiffKind::Unchanged);
        assert_eq!(diff[1].kind, DiffKind::Added);
        assert_eq!(diff[1].name, "validates uniqueness");
    }

    #[test]
    fn diff_removed_spec() {
        let base = vec![
            SpecNode::spec("validates email", 1),
            SpecNode::spec("requires password", 2),
        ];
        let head = vec![SpecNode::spec("validates email", 1)];
        let diff = diff_spec_nodes(&base, &head);
        assert_eq!(diff.len(), 2);
        assert_eq!(diff[0].kind, DiffKind::Unchanged);
        assert_eq!(diff[1].kind, DiffKind::Removed);
        assert_eq!(diff[1].name, "requires password");
    }

    #[test]
    fn diff_renamed_spec() {
        let base = vec![SpecNode::spec("validates password presence", 1)];
        let head = vec![SpecNode::spec("validates password length", 1)];
        let diff = diff_spec_nodes(&base, &head);
        assert_eq!(diff.len(), 1);
        assert_eq!(diff[0].kind, DiffKind::Renamed);
        assert_eq!(diff[0].name, "validates password length");
        assert_eq!(diff[0].old_name.as_deref(), Some("validates password presence"));
    }

    #[test]
    fn diff_modified_group() {
        let base = vec![SpecNode::group(
            "validations",
            1,
            vec![SpecNode::spec("validates email", 2)],
        )];
        let head = vec![SpecNode::group(
            "validations",
            1,
            vec![
                SpecNode::spec("validates email", 2),
                SpecNode::spec("validates uniqueness", 3),
            ],
        )];
        let diff = diff_spec_nodes(&base, &head);
        assert_eq!(diff.len(), 1);
        assert_eq!(diff[0].kind, DiffKind::Modified);
        assert_eq!(diff[0].name, "validations");
        assert_eq!(diff[0].children.len(), 2);
        assert_eq!(diff[0].children[0].kind, DiffKind::Unchanged);
        assert_eq!(diff[0].children[1].kind, DiffKind::Added);
    }

    #[test]
    fn diff_added_group() {
        let base: Vec<SpecNode> = vec![];
        let head = vec![SpecNode::group(
            "admin requests",
            1,
            vec![SpecNode::spec("returns 403", 2)],
        )];
        let diff = diff_spec_nodes(&base, &head);
        assert_eq!(diff.len(), 1);
        assert_eq!(diff[0].kind, DiffKind::Added);
        assert_eq!(diff[0].children.len(), 1);
        assert_eq!(diff[0].children[0].kind, DiffKind::Added);
    }

    fn read_fixture(rel_path: &str) -> Option<String> {
        let fixtures_dir = concat!(env!("CARGO_MANIFEST_DIR"), "/../specdiff-tests/fixtures");
        let fixtures_path = std::path::Path::new(fixtures_dir);
        if !fixtures_path.exists() {
            eprintln!("skipping fixture test: specdiff-tests repo not found");
            return None;
        }
        let full = fixtures_path.join(rel_path);
        Some(std::fs::read_to_string(&full)
            .unwrap_or_else(|e| panic!("fixture {} should be readable: {e}", full.display())))
    }

    #[test]
    fn diff_rspec_fixture_pair() {
        use crate::parse::engine::parse_file;
        use crate::parse::registry::all_frameworks;

        let Some(base_src) = read_fixture("rspec/base/spec/models/user_spec.rb") else { return };
        let Some(head_src) = read_fixture("rspec/head/spec/models/user_spec.rb") else { return };

        let rspec = all_frameworks().iter().find(|f| f.name == "rspec").expect("rspec");
        let base_tree = parse_file(&base_src, "spec/models/user_spec.rb", rspec).expect("base");
        let head_tree = parse_file(&head_src, "spec/models/user_spec.rb", rspec).expect("head");

        let diff = diff_spec_nodes(&base_tree.root, &head_tree.root);
        assert_eq!(diff.len(), 1);

        let user = &diff[0];
        assert_eq!(user.kind, DiffKind::Modified);

        let validations = &user.children[0];
        assert_eq!(validations.kind, DiffKind::Modified);

        let has_added = validations.children.iter().any(|d| d.kind == DiffKind::Added);
        let has_renamed = validations.children.iter().any(|d| d.kind == DiffKind::Renamed);
        assert!(has_added || has_renamed, "should detect changes in validations");
    }

    #[test]
    fn name_similarity_identical() {
        assert_eq!(name_similarity("foo bar", "foo bar"), 1.0);
    }

    #[test]
    fn name_similarity_partial() {
        let sim = name_similarity("requires password", "validates password length");
        assert!(sim > 0.0);
        assert!(sim < 1.0);
    }

    #[test]
    fn name_similarity_empty() {
        assert_eq!(name_similarity("", "foo"), 0.0);
        assert_eq!(name_similarity("foo", ""), 0.0);
    }

    #[test]
    fn diff_all_new_specs() {
        let base: Vec<SpecNode> = vec![];
        let head = vec![
            SpecNode::spec("test one", 1),
            SpecNode::spec("test two", 2),
        ];
        let diff = diff_spec_nodes(&base, &head);
        assert_eq!(diff.len(), 2);
        assert!(diff.iter().all(|d| d.kind == DiffKind::Added));
    }

    #[test]
    fn diff_all_deleted_specs() {
        let base = vec![
            SpecNode::spec("test one", 1),
            SpecNode::spec("test two", 2),
        ];
        let head: Vec<SpecNode> = vec![];
        let diff = diff_spec_nodes(&base, &head);
        assert_eq!(diff.len(), 2);
        assert!(diff.iter().all(|d| d.kind == DiffKind::Removed));
    }

    #[test]
    fn diff_renamed_group_shows_as_removed_and_added() {
        let base = vec![SpecNode::group(
            "User",
            1,
            vec![SpecNode::spec("has name", 2)],
        )];
        let head = vec![SpecNode::group(
            "Account",
            1,
            vec![SpecNode::spec("has name", 2)],
        )];
        let diff = diff_spec_nodes(&base, &head);
        assert_eq!(diff.len(), 2, "renamed group should be removed + added, not renamed");
        assert_eq!(diff[0].kind, DiffKind::Removed);
        assert_eq!(diff[0].name, "User");
        assert_eq!(diff[1].kind, DiffKind::Added);
        assert_eq!(diff[1].name, "Account");
    }

    #[test]
    fn diff_empty_trees() {
        let base: Vec<SpecNode> = vec![];
        let head: Vec<SpecNode> = vec![];
        let diff = diff_spec_nodes(&base, &head);
        assert!(diff.is_empty());
    }

    #[test]
    fn filter_file_diffs_by_name() {
        let diffs = vec![
            FileDiff {
                path: "models::user".into(),
                nodes: vec![
                    DiffNode { name: "validates email".into(), kind: DiffKind::Added, old_name: None, param_cases: None, children: vec![] },
                    DiffNode { name: "has many posts".into(), kind: DiffKind::Added, old_name: None, param_cases: None, children: vec![] },
                ],
            },
            FileDiff {
                path: "models::post".into(),
                nodes: vec![
                    DiffNode { name: "belongs to user".into(), kind: DiffKind::Added, old_name: None, param_cases: None, children: vec![] },
                ],
            },
        ];

        let filtered = filter_file_diffs(diffs, "email");
        assert_eq!(filtered.len(), 1);
        assert_eq!(filtered[0].path, "models::user");
        assert_eq!(filtered[0].nodes.len(), 1);
        assert_eq!(filtered[0].nodes[0].name, "validates email");
    }

    #[test]
    fn filter_file_diffs_by_path() {
        let diffs = vec![
            FileDiff {
                path: "models::user".into(),
                nodes: vec![
                    DiffNode { name: "test".into(), kind: DiffKind::Added, old_name: None, param_cases: None, children: vec![] },
                ],
            },
            FileDiff {
                path: "models::post".into(),
                nodes: vec![
                    DiffNode { name: "test".into(), kind: DiffKind::Added, old_name: None, param_cases: None, children: vec![] },
                ],
            },
        ];

        let filtered = filter_file_diffs(diffs, "post");
        assert_eq!(filtered.len(), 1);
        assert_eq!(filtered[0].path, "models::post");
    }

    #[test]
    fn filter_is_case_insensitive() {
        let diffs = vec![FileDiff {
            path: "models::user".into(),
            nodes: vec![
                DiffNode { name: "Validates Email".into(), kind: DiffKind::Added, old_name: None, param_cases: None, children: vec![] },
            ],
        }];

        let filtered = filter_file_diffs(diffs, "email");
        assert_eq!(filtered.len(), 1);
    }
}
