use crate::model::*;

fn build_sarif_envelope(results: Vec<serde_json::Value>, rules: Vec<serde_json::Value>) -> String {
    let sarif = serde_json::json!({
        "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
        "version": "2.1.0",
        "runs": [{
            "tool": {
                "driver": {
                    "name": "todo-scan",
                    "version": env!("CARGO_PKG_VERSION"),
                    "rules": rules
                }
            },
            "results": results
        }]
    });
    serde_json::to_string_pretty(&sarif).expect("failed to serialize SARIF")
}

fn rule_id(tag: &Tag) -> String {
    format!("todo-scan/{}", tag.as_str())
}

fn collect_rules(items: &[&TodoItem]) -> Vec<serde_json::Value> {
    let mut seen = std::collections::BTreeSet::new();
    let mut rules = Vec::new();
    for item in items {
        let id = rule_id(&item.tag);
        if seen.insert(id.clone()) {
            rules.push(serde_json::json!({
                "id": id,
                "shortDescription": {
                    "text": format!("{} comment", item.tag.as_str())
                }
            }));
        }
    }
    rules
}

fn item_to_result(item: &TodoItem) -> serde_json::Value {
    let severity = Severity::from_item(item);
    let mut result = serde_json::json!({
        "ruleId": rule_id(&item.tag),
        "level": severity.as_sarif_level(),
        "message": {
            "text": item.message
        },
        "locations": [{
            "physicalLocation": {
                "artifactLocation": {
                    "uri": item.file
                },
                "region": {
                    "startLine": item.line
                }
            }
        }]
    });
    if let Some(ref deadline) = item.deadline {
        result
            .as_object_mut()
            .expect("SARIF result should be a JSON object")
            .insert(
                "properties".to_string(),
                serde_json::json!({ "deadline": deadline.to_string() }),
            );
    }
    result
}

pub fn format_list(result: &ScanResult) -> String {
    let results: Vec<serde_json::Value> = result.items.iter().map(item_to_result).collect();
    let all_items: Vec<&TodoItem> = result.items.iter().collect();
    let rules = collect_rules(&all_items);
    let mut output = build_sarif_envelope(results, rules);
    output.push('\n');
    output
}

pub fn format_search(result: &SearchResult) -> String {
    let results: Vec<serde_json::Value> = result.items.iter().map(item_to_result).collect();
    let all_items: Vec<&TodoItem> = result.items.iter().collect();
    let rules = collect_rules(&all_items);
    let mut output = build_sarif_envelope(results, rules);
    output.push('\n');
    output
}

pub fn format_diff(result: &DiffResult) -> String {
    let results: Vec<serde_json::Value> = result
        .entries
        .iter()
        .map(|entry| {
            let mut r = item_to_result(&entry.item);
            let status = match entry.status {
                DiffStatus::Added => "added",
                DiffStatus::Removed => "removed",
            };
            r.as_object_mut()
                .expect("SARIF result should be a JSON object")
                .insert(
                    "properties".to_string(),
                    serde_json::json!({ "diffStatus": status }),
                );
            r
        })
        .collect();

    let all_items: Vec<&TodoItem> = result.entries.iter().map(|e| &e.item).collect();
    let rules = collect_rules(&all_items);
    let mut output = build_sarif_envelope(results, rules);
    output.push('\n');
    output
}

pub fn format_blame(result: &BlameResult) -> String {
    let results: Vec<serde_json::Value> = result
        .entries
        .iter()
        .map(|entry| {
            let mut r = item_to_result(&entry.item);
            r.as_object_mut()
                .expect("SARIF result should be a JSON object")
                .insert(
                    "properties".to_string(),
                    serde_json::json!({
                        "blame": {
                            "author": entry.blame.author,
                            "email": entry.blame.email,
                            "date": entry.blame.date,
                            "ageDays": entry.blame.age_days,
                            "commit": entry.blame.commit,
                            "stale": entry.stale,
                        }
                    }),
                );
            r
        })
        .collect();

    let all_items: Vec<&TodoItem> = result.entries.iter().map(|e| &e.item).collect();
    let rules = collect_rules(&all_items);
    let mut output = build_sarif_envelope(results, rules);
    output.push('\n');
    output
}

pub fn format_lint(result: &LintResult) -> String {
    let results: Vec<serde_json::Value> = result
        .violations
        .iter()
        .map(|v| {
            let mut r = serde_json::json!({
                "ruleId": format!("todo-scan/lint/{}", v.rule),
                "level": "error",
                "message": {
                    "text": v.message
                },
                "locations": [{
                    "physicalLocation": {
                        "artifactLocation": {
                            "uri": v.file
                        },
                        "region": {
                            "startLine": v.line
                        }
                    }
                }]
            });
            if let Some(ref suggestion) = v.suggestion {
                r.as_object_mut()
                    .expect("SARIF result should be a JSON object")
                    .insert(
                        "fixes".to_string(),
                        serde_json::json!([{
                            "description": {
                                "text": suggestion
                            }
                        }]),
                    );
            }
            r
        })
        .collect();

    let mut seen = std::collections::BTreeSet::new();
    let rules: Vec<serde_json::Value> = result
        .violations
        .iter()
        .filter_map(|v| {
            let id = format!("todo-scan/lint/{}", v.rule);
            if seen.insert(id.clone()) {
                Some(serde_json::json!({
                    "id": id,
                    "shortDescription": {
                        "text": format!("{} lint rule", v.rule)
                    }
                }))
            } else {
                None
            }
        })
        .collect();

    let final_results = if result.passed && results.is_empty() {
        vec![serde_json::json!({
            "ruleId": "todo-scan/lint/summary",
            "level": "note",
            "message": {
                "text": format!("All lint checks passed ({} items)", result.total_items)
            }
        })]
    } else {
        results
    };

    let final_rules = if result.passed && rules.is_empty() {
        vec![serde_json::json!({
            "id": "todo-scan/lint/summary",
            "shortDescription": {
                "text": "todo-scan lint summary"
            }
        })]
    } else {
        rules
    };

    let mut output = build_sarif_envelope(final_results, final_rules);
    output.push('\n');
    output
}

pub fn format_check(result: &CheckResult) -> String {
    let results: Vec<serde_json::Value> = result
        .violations
        .iter()
        .map(|v| {
            serde_json::json!({
                "ruleId": format!("todo-scan/check/{}", v.rule),
                "level": if result.passed { "note" } else { "error" },
                "message": {
                    "text": v.message
                }
            })
        })
        .collect();

    let rules: Vec<serde_json::Value> = result
        .violations
        .iter()
        .map(|v| {
            serde_json::json!({
                "id": format!("todo-scan/check/{}", v.rule),
                "shortDescription": {
                    "text": format!("{} check", v.rule)
                }
            })
        })
        .collect();

    // If passed with no violations, add a summary result
    let final_results = if result.passed && results.is_empty() {
        vec![serde_json::json!({
            "ruleId": "todo-scan/check/summary",
            "level": "note",
            "message": {
                "text": format!("All checks passed ({} items total)", result.total)
            }
        })]
    } else {
        results
    };

    let final_rules = if result.passed && rules.is_empty() {
        vec![serde_json::json!({
            "id": "todo-scan/check/summary",
            "shortDescription": {
                "text": "todo-scan check summary"
            }
        })]
    } else {
        rules
    };

    let mut output = build_sarif_envelope(final_results, final_rules);
    output.push('\n');
    output
}

pub fn format_clean(result: &CleanResult) -> String {
    let results: Vec<serde_json::Value> = result
        .violations
        .iter()
        .map(|v| {
            let mut r = serde_json::json!({
                "ruleId": format!("todo-scan/clean/{}", v.rule),
                "level": "error",
                "message": {
                    "text": v.message
                },
                "locations": [{
                    "physicalLocation": {
                        "artifactLocation": {
                            "uri": v.file
                        },
                        "region": {
                            "startLine": v.line
                        }
                    }
                }]
            });
            let mut props = serde_json::Map::new();
            if let Some(ref issue_ref) = v.issue_ref {
                props.insert(
                    "issueRef".to_string(),
                    serde_json::Value::String(issue_ref.clone()),
                );
            }
            if let Some(ref duplicate_of) = v.duplicate_of {
                props.insert(
                    "duplicateOf".to_string(),
                    serde_json::Value::String(duplicate_of.clone()),
                );
            }
            if !props.is_empty() {
                r.as_object_mut()
                    .unwrap()
                    .insert("properties".to_string(), serde_json::Value::Object(props));
            }
            r
        })
        .collect();

    let mut seen = std::collections::BTreeSet::new();
    let rules: Vec<serde_json::Value> = result
        .violations
        .iter()
        .filter_map(|v| {
            let id = format!("todo-scan/clean/{}", v.rule);
            if seen.insert(id.clone()) {
                Some(serde_json::json!({
                    "id": id,
                    "shortDescription": {
                        "text": format!("{} clean rule", v.rule)
                    }
                }))
            } else {
                None
            }
        })
        .collect();

    let final_results = if result.passed && results.is_empty() {
        vec![serde_json::json!({
            "ruleId": "todo-scan/clean/summary",
            "level": "note",
            "message": {
                "text": format!("All clean checks passed ({} items)", result.total_items)
            }
        })]
    } else {
        results
    };

    let final_rules = if result.passed && rules.is_empty() {
        vec![serde_json::json!({
            "id": "todo-scan/clean/summary",
            "shortDescription": {
                "text": "todo-scan clean summary"
            }
        })]
    } else {
        rules
    };

    let mut output = build_sarif_envelope(final_results, final_rules);
    output.push('\n');
    output
}

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

    fn sample_item(tag: Tag, message: &str) -> TodoItem {
        TodoItem {
            file: "src/main.rs".to_string(),
            line: 10,
            tag,
            message: message.to_string(),
            author: None,
            issue_ref: None,
            priority: Priority::Normal,
            deadline: None,
        }
    }

    #[test]
    fn test_format_list_sarif_structure() {
        let result = ScanResult {
            items: vec![sample_item(Tag::Todo, "implement feature")],
            files_scanned: 1,
            ignored_items: vec![],
        };
        let output = format_list(&result);
        let sarif: serde_json::Value = serde_json::from_str(&output).unwrap();

        assert_eq!(sarif["version"], "2.1.0");
        assert_eq!(sarif["runs"][0]["tool"]["driver"]["name"], "todo-scan");

        let results = sarif["runs"][0]["results"].as_array().unwrap();
        assert_eq!(results.len(), 1);
        assert_eq!(results[0]["ruleId"], "todo-scan/TODO");
        assert_eq!(results[0]["level"], "warning");
        assert_eq!(results[0]["message"]["text"], "implement feature");
        assert_eq!(
            results[0]["locations"][0]["physicalLocation"]["artifactLocation"]["uri"],
            "src/main.rs"
        );
        assert_eq!(
            results[0]["locations"][0]["physicalLocation"]["region"]["startLine"],
            10
        );
    }

    #[test]
    fn test_format_list_sarif_severity() {
        let result = ScanResult {
            items: vec![
                sample_item(Tag::Bug, "critical"),
                sample_item(Tag::Note, "info"),
            ],
            files_scanned: 1,
            ignored_items: vec![],
        };
        let output = format_list(&result);
        let sarif: serde_json::Value = serde_json::from_str(&output).unwrap();
        let results = sarif["runs"][0]["results"].as_array().unwrap();
        assert_eq!(results[0]["level"], "error");
        assert_eq!(results[1]["level"], "note");
    }

    #[test]
    fn test_format_list_sarif_rules_deduplication() {
        let result = ScanResult {
            items: vec![
                sample_item(Tag::Todo, "first"),
                sample_item(Tag::Todo, "second"),
                sample_item(Tag::Bug, "a bug"),
            ],
            files_scanned: 1,
            ignored_items: vec![],
        };
        let output = format_list(&result);
        let sarif: serde_json::Value = serde_json::from_str(&output).unwrap();
        let rules = sarif["runs"][0]["tool"]["driver"]["rules"]
            .as_array()
            .unwrap();
        assert_eq!(rules.len(), 2); // TODO and BUG, not 3
    }

    #[test]
    fn test_format_diff_sarif_has_diff_status() {
        let result = DiffResult {
            entries: vec![DiffEntry {
                status: DiffStatus::Added,
                item: sample_item(Tag::Fixme, "new fix"),
            }],
            added_count: 1,
            removed_count: 0,
            base_ref: "main".to_string(),
        };
        let output = format_diff(&result);
        let sarif: serde_json::Value = serde_json::from_str(&output).unwrap();
        let results = sarif["runs"][0]["results"].as_array().unwrap();
        assert_eq!(results[0]["properties"]["diffStatus"], "added");
    }

    #[test]
    fn test_format_check_sarif_pass() {
        let result = CheckResult {
            passed: true,
            total: 5,
            violations: vec![],
        };
        let output = format_check(&result);
        let sarif: serde_json::Value = serde_json::from_str(&output).unwrap();
        let results = sarif["runs"][0]["results"].as_array().unwrap();
        assert_eq!(results.len(), 1);
        assert_eq!(results[0]["level"], "note");
        assert!(results[0]["message"]["text"]
            .as_str()
            .unwrap()
            .contains("passed"));
    }

    #[test]
    fn test_format_check_sarif_fail() {
        let result = CheckResult {
            passed: false,
            total: 10,
            violations: vec![CheckViolation {
                rule: "max".to_string(),
                message: "10 exceeds max 5".to_string(),
            }],
        };
        let output = format_check(&result);
        let sarif: serde_json::Value = serde_json::from_str(&output).unwrap();
        let results = sarif["runs"][0]["results"].as_array().unwrap();
        assert_eq!(results[0]["ruleId"], "todo-scan/check/max");
        assert_eq!(results[0]["level"], "error");
    }
}
