//! SARIF 2.1.0 output for GitHub Code Scanning integration.
//!
//! Provides structured SARIF reports for validation errors, lint findings,
//! safety threats, and migration risks. Each converter function produces a
//! complete SARIF report that can be uploaded to GitHub Code Scanning.
//!
//! # SARIF Specification
//!
//! Follows the OASIS SARIF 2.1.0 standard:
//! <https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html>

use serde::Serialize;

use crate::errors::types::{ErrorCode, ValidationError, ValidationResult};
use crate::linter::types::{LintFinding, LintResult, LintSeverity};
use crate::migration::types::{MigrationFinding, MigrationResult, MigrationRisk};
use crate::safety::types::{SafetyScanResult, SafetySeverity, ThreatFinding};

/// SARIF schema URL for version 2.1.0.
const SARIF_SCHEMA: &str = "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json";

/// SARIF version string.
const SARIF_VERSION: &str = "2.1.0";

/// A complete SARIF 2.1.0 report.
#[derive(Debug, Clone, Serialize)]
pub struct SarifReport {
    /// JSON schema reference.
    #[serde(rename = "$schema")]
    pub schema: String,
    /// SARIF specification version.
    pub version: String,
    /// Analysis runs (typically one per invocation).
    pub runs: Vec<SarifRun>,
}

/// A single analysis run within a SARIF report.
#[derive(Debug, Clone, Serialize)]
pub struct SarifRun {
    /// The tool that produced the results.
    pub tool: SarifTool,
    /// Individual findings.
    pub results: Vec<SarifResult>,
}

/// Tool metadata for the SARIF run.
#[derive(Debug, Clone, Serialize)]
pub struct SarifTool {
    /// The tool driver (name, version, rules).
    pub driver: SarifDriver,
}

/// SARIF tool driver with rule definitions.
#[derive(Debug, Clone, Serialize)]
pub struct SarifDriver {
    /// Tool name.
    pub name: String,
    /// Tool version.
    pub version: String,
    /// Tool information URI.
    #[serde(rename = "informationUri")]
    pub information_uri: String,
    /// Rule definitions referenced by results.
    pub rules: Vec<SarifRule>,
}

/// A rule definition in SARIF.
#[derive(Debug, Clone, Serialize)]
pub struct SarifRule {
    /// Rule identifier (e.g., "E001", "L001", "multi_statement_injection").
    pub id: String,
    /// Short description of the rule.
    #[serde(rename = "shortDescription")]
    pub short_description: SarifMessage,
    /// Full description of the rule.
    #[serde(rename = "fullDescription")]
    pub full_description: SarifMessage,
    /// Default configuration (severity level).
    #[serde(rename = "defaultConfiguration")]
    pub default_configuration: SarifDefaultConfiguration,
}

/// Default configuration for a SARIF rule.
#[derive(Debug, Clone, Serialize)]
pub struct SarifDefaultConfiguration {
    /// Default severity level: "error", "warning", or "note".
    pub level: String,
}

/// A single finding/result in the SARIF report.
#[derive(Debug, Clone, Serialize)]
pub struct SarifResult {
    /// Rule ID this result refers to.
    #[serde(rename = "ruleId")]
    pub rule_id: String,
    /// Severity level: "error", "warning", or "note".
    pub level: String,
    /// Human-readable message.
    pub message: SarifMessage,
    /// Physical locations in source files.
    pub locations: Vec<SarifLocation>,
}

/// A SARIF message container.
#[derive(Debug, Clone, Serialize)]
pub struct SarifMessage {
    /// The message text.
    pub text: String,
}

/// A source location in SARIF.
#[derive(Debug, Clone, Serialize)]
pub struct SarifLocation {
    /// Physical location in a file.
    #[serde(rename = "physicalLocation")]
    pub physical_location: SarifPhysicalLocation,
}

/// Physical file location with region info.
#[derive(Debug, Clone, Serialize)]
pub struct SarifPhysicalLocation {
    /// Artifact (file) location.
    #[serde(rename = "artifactLocation")]
    pub artifact_location: SarifArtifactLocation,
    /// Region within the file.
    pub region: SarifRegion,
}

/// File path reference in SARIF.
#[derive(Debug, Clone, Serialize)]
pub struct SarifArtifactLocation {
    /// URI of the artifact (file path).
    pub uri: String,
}

/// Region within a source file.
#[derive(Debug, Clone, Serialize)]
pub struct SarifRegion {
    /// 1-based start line.
    #[serde(rename = "startLine")]
    pub start_line: usize,
    /// 1-based start column (optional).
    #[serde(rename = "startColumn", skip_serializing_if = "Option::is_none")]
    pub start_column: Option<usize>,
}

// ---------------------------------------------------------------------------
// Builder helpers
// ---------------------------------------------------------------------------

fn new_report(tool_name: &str, rules: Vec<SarifRule>, results: Vec<SarifResult>) -> SarifReport {
    SarifReport {
        schema: SARIF_SCHEMA.to_string(),
        version: SARIF_VERSION.to_string(),
        runs: vec![SarifRun {
            tool: SarifTool {
                driver: SarifDriver {
                    name: tool_name.to_string(),
                    version: env!("CARGO_PKG_VERSION").to_string(),
                    information_uri: "https://github.com/AltimateAI/altimate-core".to_string(),
                    rules,
                },
            },
            results,
        }],
    }
}

fn make_location(file: &str, line: usize, column: Option<usize>) -> SarifLocation {
    SarifLocation {
        physical_location: SarifPhysicalLocation {
            artifact_location: SarifArtifactLocation {
                uri: file.to_string(),
            },
            region: SarifRegion {
                start_line: if line == 0 { 1 } else { line },
                start_column: column,
            },
        },
    }
}

// ---------------------------------------------------------------------------
// Validation errors -> SARIF
// ---------------------------------------------------------------------------

fn error_code_rule(code: &ErrorCode) -> SarifRule {
    let (short, full) = match code {
        ErrorCode::E000 => (
            "SQL syntax error",
            "The SQL query contains a syntax error that prevents parsing.",
        ),
        ErrorCode::E001 => (
            "Table not found",
            "A table referenced in the query does not exist in the schema.",
        ),
        ErrorCode::E002 => (
            "Column not found",
            "A column referenced in the query does not exist in the referenced table.",
        ),
        ErrorCode::E003 => ("Type mismatch", "An expression uses incompatible types."),
        ErrorCode::E004 => (
            "Ambiguous column",
            "A column name is ambiguous because it exists in multiple tables.",
        ),
        ErrorCode::E005 => (
            "Invalid join",
            "The join condition is invalid or references incorrect columns.",
        ),
        ErrorCode::E006 => (
            "Unsupported dialect feature",
            "The query uses a feature not supported in the target SQL dialect.",
        ),
        ErrorCode::E099 => (
            "Semantic error",
            "A semantic error was detected during query validation.",
        ),
    };
    SarifRule {
        id: code.to_string(),
        short_description: SarifMessage {
            text: short.to_string(),
        },
        full_description: SarifMessage {
            text: full.to_string(),
        },
        default_configuration: SarifDefaultConfiguration {
            level: "error".to_string(),
        },
    }
}

fn validation_error_to_result(error: &ValidationError, file: &str) -> SarifResult {
    let (line, col) = error
        .location
        .as_ref()
        .map(|loc| (loc.line, Some(loc.column)))
        .unwrap_or((1, None));

    SarifResult {
        rule_id: error.code.to_string(),
        level: "error".to_string(),
        message: SarifMessage {
            text: error.message.clone(),
        },
        locations: vec![make_location(file, line, col)],
    }
}

/// Convert a [`ValidationResult`] into a SARIF 2.1.0 report.
///
/// Each validation error becomes a SARIF result keyed by its error code
/// (E000-E099). Valid queries produce a report with zero results.
pub fn validation_to_sarif(result: &ValidationResult, file: &str) -> SarifReport {
    let mut seen_codes = std::collections::HashSet::new();
    let mut rules = Vec::new();
    let mut results = Vec::new();

    for error in &result.errors {
        if seen_codes.insert(error.code) {
            rules.push(error_code_rule(&error.code));
        }
        results.push(validation_error_to_result(error, file));
    }

    new_report("altimate-core", rules, results)
}

// ---------------------------------------------------------------------------
// Lint findings -> SARIF
// ---------------------------------------------------------------------------

fn lint_severity_to_level(severity: &LintSeverity) -> &'static str {
    match severity {
        LintSeverity::Error => "error",
        LintSeverity::Warning => "warning",
        LintSeverity::Info => "note",
    }
}

fn lint_finding_to_rule(finding: &LintFinding) -> SarifRule {
    SarifRule {
        id: finding.code.clone(),
        short_description: SarifMessage {
            text: finding.rule.clone(),
        },
        full_description: SarifMessage {
            text: finding.message.clone(),
        },
        default_configuration: SarifDefaultConfiguration {
            level: lint_severity_to_level(&finding.severity).to_string(),
        },
    }
}

fn lint_finding_to_result(finding: &LintFinding, file: &str) -> SarifResult {
    SarifResult {
        rule_id: finding.code.clone(),
        level: lint_severity_to_level(&finding.severity).to_string(),
        message: SarifMessage {
            text: finding.message.clone(),
        },
        locations: vec![make_location(
            file,
            finding.line.unwrap_or(1),
            finding.column,
        )],
    }
}

/// Convert a [`LintResult`] into a SARIF 2.1.0 report.
///
/// Each lint finding becomes a SARIF result keyed by its rule code
/// (L001-L026). Clean queries produce a report with zero results.
pub fn lint_to_sarif(result: &LintResult, file: &str) -> SarifReport {
    let mut seen_codes = std::collections::HashSet::new();
    let mut rules = Vec::new();
    let mut results = Vec::new();

    for finding in &result.findings {
        if seen_codes.insert(finding.code.clone()) {
            rules.push(lint_finding_to_rule(finding));
        }
        results.push(lint_finding_to_result(finding, file));
    }

    new_report("altimate-core", rules, results)
}

// ---------------------------------------------------------------------------
// Safety threats -> SARIF
// ---------------------------------------------------------------------------

fn safety_severity_to_level(severity: &SafetySeverity) -> &'static str {
    match severity {
        SafetySeverity::Critical => "error",
        SafetySeverity::High => "error",
        SafetySeverity::Medium => "warning",
        SafetySeverity::Low => "note",
    }
}

fn threat_to_rule(threat: &ThreatFinding) -> SarifRule {
    SarifRule {
        id: format!("safety/{}", threat.rule.name()),
        short_description: SarifMessage {
            text: threat.message.clone(),
        },
        full_description: SarifMessage {
            text: threat.detail.clone(),
        },
        default_configuration: SarifDefaultConfiguration {
            level: safety_severity_to_level(&threat.severity).to_string(),
        },
    }
}

fn threat_to_result(threat: &ThreatFinding, file: &str) -> SarifResult {
    // Compute line number from byte offset if location is available
    let line = threat.location.map(|(_offset, _len)| 1_usize).unwrap_or(1);

    SarifResult {
        rule_id: format!("safety/{}", threat.rule.name()),
        level: safety_severity_to_level(&threat.severity).to_string(),
        message: SarifMessage {
            text: threat.message.clone(),
        },
        locations: vec![make_location(file, line, None)],
    }
}

/// Convert a [`SafetyScanResult`] into a SARIF 2.1.0 report.
///
/// Each detected threat becomes a SARIF result keyed by the safety rule name
/// (e.g., `safety/tautology_attack`). Safe queries produce zero results.
pub fn safety_to_sarif(result: &SafetyScanResult, file: &str) -> SarifReport {
    let mut seen_rules = std::collections::HashSet::new();
    let mut rules = Vec::new();
    let mut results = Vec::new();

    for threat in &result.threats {
        let rule_id = format!("safety/{}", threat.rule.name());
        if seen_rules.insert(rule_id) {
            rules.push(threat_to_rule(threat));
        }
        results.push(threat_to_result(threat, file));
    }

    new_report("altimate-core", rules, results)
}

// ---------------------------------------------------------------------------
// Migration risks -> SARIF
// ---------------------------------------------------------------------------

fn migration_risk_to_level(risk: &MigrationRisk) -> &'static str {
    match risk {
        MigrationRisk::Destructive => "error",
        MigrationRisk::Dangerous => "error",
        MigrationRisk::Caution => "warning",
        MigrationRisk::Safe => "note",
    }
}

fn migration_finding_to_rule(finding: &MigrationFinding) -> SarifRule {
    let rule_id = format!(
        "migration/{}",
        finding.operation.to_lowercase().replace(' ', "_")
    );
    SarifRule {
        id: rule_id,
        short_description: SarifMessage {
            text: finding.operation.clone(),
        },
        full_description: SarifMessage {
            text: finding.message.clone(),
        },
        default_configuration: SarifDefaultConfiguration {
            level: migration_risk_to_level(&finding.risk).to_string(),
        },
    }
}

fn migration_finding_to_result(finding: &MigrationFinding, file: &str) -> SarifResult {
    let rule_id = format!(
        "migration/{}",
        finding.operation.to_lowercase().replace(' ', "_")
    );
    SarifResult {
        rule_id,
        level: migration_risk_to_level(&finding.risk).to_string(),
        message: SarifMessage {
            text: finding.message.clone(),
        },
        locations: vec![make_location(file, 1, None)],
    }
}

/// Convert a [`MigrationResult`] into a SARIF 2.1.0 report.
///
/// Each migration finding becomes a SARIF result keyed by its operation
/// (e.g., `migration/drop_column`). Safe migrations produce zero results
/// only when the overall risk is Safe.
pub fn migration_to_sarif(result: &MigrationResult, file: &str) -> SarifReport {
    let mut seen_rules = std::collections::HashSet::new();
    let mut rules = Vec::new();
    let mut results = Vec::new();

    for finding in &result.findings {
        let rule_id = format!(
            "migration/{}",
            finding.operation.to_lowercase().replace(' ', "_")
        );
        if seen_rules.insert(rule_id) {
            rules.push(migration_finding_to_rule(finding));
        }
        results.push(migration_finding_to_result(finding, file));
    }

    new_report("altimate-core", rules, results)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::errors::types::{ErrorKind, SourceLocation};

    #[test]
    fn test_validation_sarif_schema_and_version() {
        let result = ValidationResult::ok(Default::default());
        let sarif = validation_to_sarif(&result, "test.sql");
        assert_eq!(sarif.version, "2.1.0");
        assert_eq!(sarif.schema, SARIF_SCHEMA);
        assert!(sarif.runs[0].results.is_empty());
    }

    #[test]
    fn test_validation_sarif_with_errors() {
        let result = ValidationResult::with_errors(vec![
            ValidationError {
                code: ErrorCode::E001,
                kind: ErrorKind::TableNotFound {
                    table: "users".to_string(),
                },
                message: "Table 'users' not found".to_string(),
                location: Some(SourceLocation {
                    line: 1,
                    column: 15,
                }),
                suggestions: vec![],
            },
            ValidationError {
                code: ErrorCode::E002,
                kind: ErrorKind::ColumnNotFound {
                    table: Some("orders".to_string()),
                    column: "user_id".to_string(),
                },
                message: "Column 'user_id' not found in 'orders'".to_string(),
                location: Some(SourceLocation { line: 2, column: 8 }),
                suggestions: vec![],
            },
        ]);
        let sarif = validation_to_sarif(&result, "query.sql");
        assert_eq!(sarif.runs[0].results.len(), 2);
        assert_eq!(sarif.runs[0].tool.driver.rules.len(), 2);
        assert_eq!(sarif.runs[0].results[0].rule_id, "E001");
        assert_eq!(sarif.runs[0].results[0].level, "error");
        assert_eq!(
            sarif.runs[0].results[0].locations[0]
                .physical_location
                .region
                .start_line,
            1
        );
        assert_eq!(
            sarif.runs[0].results[0].locations[0]
                .physical_location
                .region
                .start_column,
            Some(15)
        );
    }

    #[test]
    fn test_lint_sarif_empty() {
        let result = LintResult {
            sql: "SELECT id FROM users".to_string(),
            findings: vec![],
            error_count: 0,
            warning_count: 0,
            clean: true,
        };
        let sarif = lint_to_sarif(&result, "test.sql");
        assert!(sarif.runs[0].results.is_empty());
    }

    #[test]
    fn test_lint_sarif_with_findings() {
        let result = LintResult {
            sql: "SELECT * FROM users".to_string(),
            findings: vec![LintFinding {
                code: "L001".to_string(),
                rule: "select_star".to_string(),
                severity: LintSeverity::Warning,
                message: "Avoid SELECT *".to_string(),
                suggestion: Some("List columns explicitly".to_string()),
                line: Some(1),
                column: Some(8),
            }],
            error_count: 0,
            warning_count: 1,
            clean: false,
        };
        let sarif = lint_to_sarif(&result, "test.sql");
        assert_eq!(sarif.runs[0].results.len(), 1);
        assert_eq!(sarif.runs[0].results[0].rule_id, "L001");
        assert_eq!(sarif.runs[0].results[0].level, "warning");
    }

    #[test]
    fn test_safety_sarif_safe() {
        let result = SafetyScanResult {
            safe: true,
            threats: vec![],
            risk_score: 0.0,
            statement_count: 1,
            statement_types: vec!["SELECT".to_string()],
        };
        let sarif = safety_to_sarif(&result, "test.sql");
        assert!(sarif.runs[0].results.is_empty());
    }

    #[test]
    fn test_safety_sarif_with_threats() {
        use crate::safety::types::{SafetyRule, SafetySeverity};
        let result = SafetyScanResult {
            safe: false,
            threats: vec![ThreatFinding {
                rule: SafetyRule::TautologyAttack,
                severity: SafetySeverity::High,
                message: "Tautology detected: OR 1=1".to_string(),
                detail: "Always-true condition bypasses WHERE clause".to_string(),
                location: Some((30, 6)),
                matched_pattern: "1=1".to_string(),
            }],
            risk_score: 0.8,
            statement_count: 1,
            statement_types: vec!["SELECT".to_string()],
        };
        let sarif = safety_to_sarif(&result, "test.sql");
        assert_eq!(sarif.runs[0].results.len(), 1);
        assert_eq!(sarif.runs[0].results[0].rule_id, "safety/tautology_attack");
        assert_eq!(sarif.runs[0].results[0].level, "error");
    }

    #[test]
    fn test_migration_sarif_safe() {
        let result = MigrationResult {
            sql: "ALTER TABLE users ADD COLUMN age INT".to_string(),
            overall_risk: MigrationRisk::Safe,
            findings: vec![],
            safe: true,
            rollback_sql: None,
        };
        let sarif = migration_to_sarif(&result, "migration.sql");
        assert!(sarif.runs[0].results.is_empty());
    }

    #[test]
    fn test_migration_sarif_with_risks() {
        let result = MigrationResult {
            sql: "ALTER TABLE users DROP COLUMN email".to_string(),
            overall_risk: MigrationRisk::Destructive,
            findings: vec![MigrationFinding {
                risk: MigrationRisk::Destructive,
                operation: "DROP COLUMN".to_string(),
                message: "Dropping column 'email' causes irreversible data loss".to_string(),
                mitigation: Some("Back up data before dropping".to_string()),
                rollback_sql: Some("ALTER TABLE users ADD COLUMN email VARCHAR".to_string()),
            }],
            safe: false,
            rollback_sql: Some("ALTER TABLE users ADD COLUMN email VARCHAR".to_string()),
        };
        let sarif = migration_to_sarif(&result, "migration.sql");
        assert_eq!(sarif.runs[0].results.len(), 1);
        assert_eq!(sarif.runs[0].results[0].rule_id, "migration/drop_column");
        assert_eq!(sarif.runs[0].results[0].level, "error");
    }

    #[test]
    fn test_sarif_serializes_to_valid_json() {
        let result = ValidationResult::with_errors(vec![ValidationError {
            code: ErrorCode::E000,
            kind: ErrorKind::SyntaxError,
            message: "Unexpected token".to_string(),
            location: None,
            suggestions: vec![],
        }]);
        let sarif = validation_to_sarif(&result, "test.sql");
        let json = serde_json::to_string_pretty(&sarif).expect("SARIF should serialize to JSON");
        assert!(json.contains("\"$schema\""));
        assert!(json.contains("\"version\": \"2.1.0\""));
        assert!(json.contains("\"ruleId\": \"E000\""));
    }

    #[test]
    fn test_deduplicates_rules() {
        let result = ValidationResult::with_errors(vec![
            ValidationError {
                code: ErrorCode::E001,
                kind: ErrorKind::TableNotFound {
                    table: "a".to_string(),
                },
                message: "Table 'a' not found".to_string(),
                location: None,
                suggestions: vec![],
            },
            ValidationError {
                code: ErrorCode::E001,
                kind: ErrorKind::TableNotFound {
                    table: "b".to_string(),
                },
                message: "Table 'b' not found".to_string(),
                location: None,
                suggestions: vec![],
            },
        ]);
        let sarif = validation_to_sarif(&result, "test.sql");
        // Two results but only one rule definition
        assert_eq!(sarif.runs[0].results.len(), 2);
        assert_eq!(sarif.runs[0].tool.driver.rules.len(), 1);
    }
}
