//! Comprehensive tests for the SQL auto-fixer.
//!
//! Tests cover: fix strategies, SQL rewriter, iterative fix engine,
//! fixture-based integration tests, configuration edge cases, and
//! serialization roundtrips.

use altimate_core::errors::types::{
    ErrorCode, ErrorKind, SourceLocation, Suggestion, SuggestionKind, ValidationError,
};
use altimate_core::fixer::engine::{fix, fix_default};
use altimate_core::fixer::rewriter::SqlRewriter;
use altimate_core::fixer::strategies::find_fix;
use altimate_core::fixer::types::{FixAction, FixActionType, FixConfig, FixResult};
use altimate_core::schema::types::SchemaDefinition;
use std::path::Path;

// ─── Helpers ──────────────────────────────────────────────────────────

fn ecommerce_schema() -> SchemaDefinition {
    SchemaDefinition::from_yaml(include_str!(
        "../../../tests/fixtures/schemas/ecommerce.yaml"
    ))
    .expect("Failed to load ecommerce schema fixture")
}

fn load_fixer_fixture(name: &str) -> String {
    std::fs::read_to_string(
        Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("../../tests/fixtures/fixer")
            .join(name),
    )
    .unwrap_or_else(|_| panic!("Failed to load fixer fixture: {}", name))
}

fn make_fix_action(original: &str, replacement: &str) -> FixAction {
    FixAction {
        action: FixActionType::ReplaceTable,
        original: original.to_string(),
        replacement: replacement.to_string(),
        confidence: 0.95,
        explanation: "test fix".to_string(),
        location: None,
    }
}

fn make_column_fix_action(original: &str, replacement: &str) -> FixAction {
    FixAction {
        action: FixActionType::ReplaceColumn,
        original: original.to_string(),
        replacement: replacement.to_string(),
        confidence: 0.90,
        explanation: "test column fix".to_string(),
        location: None,
    }
}

fn make_schema() -> SchemaDefinition {
    SchemaDefinition::from_yaml(
        r#"
version: "1"
tables:
  users:
    columns:
      - name: id
        type: INT
      - name: email
        type: VARCHAR
      - name: created_at
        type: TIMESTAMP
"#,
    )
    .expect("test schema")
}

fn make_config() -> FixConfig {
    FixConfig::default()
}

// ─── Strategy Tests ─────────────────────────────────────────────────

#[test]
fn strategy_table_not_found_with_suggestion_returns_replace_table() {
    let error = ValidationError {
        code: ErrorCode::E001,
        kind: ErrorKind::TableNotFound {
            table: "user".to_string(),
        },
        message: "Table 'user' not found".to_string(),
        location: None,
        suggestions: vec![Suggestion {
            kind: SuggestionKind::DidYouMean {
                name: "users".to_string(),
            },
            message: "Did you mean \"users\"?".to_string(),
            confidence: 0.95,
        }],
    };

    let fix = find_fix(&error, &make_schema(), &make_config());
    assert!(fix.is_some(), "Expected a fix for table typo");
    let fix = fix.unwrap();
    assert!(matches!(fix.action, FixActionType::ReplaceTable));
    assert_eq!(fix.original, "user");
    assert_eq!(fix.replacement, "users");
    assert!(fix.confidence >= 0.8);
}

#[test]
fn strategy_column_not_found_with_suggestion_returns_replace_column() {
    let error = ValidationError {
        code: ErrorCode::E002,
        kind: ErrorKind::ColumnNotFound {
            table: None,
            column: "created_date".to_string(),
        },
        message: "Column 'created_date' not found".to_string(),
        location: None,
        suggestions: vec![Suggestion {
            kind: SuggestionKind::DidYouMean {
                name: "users.created_at".to_string(),
            },
            message: "Did you mean \"users.created_at\"?".to_string(),
            confidence: 0.88,
        }],
    };

    let fix = find_fix(&error, &make_schema(), &make_config());
    assert!(fix.is_some(), "Expected a fix for column typo");
    let fix = fix.unwrap();
    assert!(matches!(fix.action, FixActionType::ReplaceColumn));
    assert_eq!(fix.original, "created_date");
    assert_eq!(fix.replacement, "created_at");
}

#[test]
fn strategy_syntax_error_returns_none() {
    let error = ValidationError {
        code: ErrorCode::E000,
        kind: ErrorKind::SyntaxError,
        message: "Syntax error near 'SELECTZ'".to_string(),
        location: None,
        suggestions: vec![],
    };

    let fix = find_fix(&error, &make_schema(), &make_config());
    assert!(fix.is_none(), "Syntax errors should be unfixable");
}

#[test]
fn strategy_low_confidence_suggestion_returns_none() {
    let error = ValidationError {
        code: ErrorCode::E001,
        kind: ErrorKind::TableNotFound {
            table: "xyz".to_string(),
        },
        message: "Table 'xyz' not found".to_string(),
        location: None,
        suggestions: vec![Suggestion {
            kind: SuggestionKind::DidYouMean {
                name: "users".to_string(),
            },
            message: "Did you mean \"users\"?".to_string(),
            confidence: 0.5, // Below threshold
        }],
    };

    let fix = find_fix(&error, &make_schema(), &make_config());
    assert!(
        fix.is_none(),
        "Low confidence suggestions should not produce fixes"
    );
}

#[test]
fn strategy_ambiguous_column_with_qualification_returns_qualify() {
    let error = ValidationError {
        code: ErrorCode::E004,
        kind: ErrorKind::AmbiguousColumn {
            column: "id".to_string(),
            candidates: vec!["users.id".to_string(), "orders.id".to_string()],
        },
        message: "Ambiguous column 'id'".to_string(),
        location: None,
        suggestions: vec![Suggestion {
            kind: SuggestionKind::AddQualification {
                qualified: "users.id".to_string(),
            },
            message: "Qualify as 'users.id'".to_string(),
            confidence: 0.9,
        }],
    };

    let fix = find_fix(&error, &make_schema(), &make_config());
    assert!(fix.is_some(), "Expected a fix for ambiguous column");
    let fix = fix.unwrap();
    assert!(matches!(fix.action, FixActionType::QualifyColumn));
    assert_eq!(fix.original, "id");
    assert_eq!(fix.replacement, "users.id");
}

// ─── SqlRewriter Tests ──────────────────────────────────────────────

#[test]
fn rewriter_single_replacement() {
    let mut rewriter = SqlRewriter::new("SELECT * FROM usrs");
    rewriter.apply(&make_fix_action("usrs", "users"));
    let result = rewriter.finish();
    assert_eq!(result, "SELECT * FROM users");
}

#[test]
fn rewriter_multiple_non_overlapping_replacements() {
    let mut rewriter = SqlRewriter::new("SELECT emal FROM usrs");
    rewriter.apply(&make_fix_action("usrs", "users"));
    rewriter.apply(&make_column_fix_action("emal", "email"));
    let result = rewriter.finish();
    assert_eq!(result, "SELECT email FROM users");
}

#[test]
fn rewriter_reverse_order_preserves_positions() {
    // Apply in the "wrong" order — the rewriter should handle it
    let mut rewriter = SqlRewriter::new("SELECT emal FROM usrs");
    rewriter.apply(&make_column_fix_action("emal", "email"));
    rewriter.apply(&make_fix_action("usrs", "users"));
    let result = rewriter.finish();
    assert_eq!(result, "SELECT email FROM users");
}

#[test]
fn rewriter_overlapping_replacements_skip_second() {
    // Two replacements targeting the same region — first one wins by position
    let mut rewriter = SqlRewriter::new("SELECT * FROM usrs");
    rewriter.apply(&make_fix_action("usrs", "users"));
    rewriter.apply(&make_fix_action("usrs", "orders")); // same target, should be skipped
    let result = rewriter.finish();
    // One of them should be applied, both target same word
    assert!(
        result == "SELECT * FROM users" || result == "SELECT * FROM orders",
        "One replacement should win, got: {}",
        result
    );
}

#[test]
fn rewriter_case_insensitive_text_search() {
    let mut rewriter = SqlRewriter::new("SELECT * FROM USRS");
    rewriter.apply(&make_fix_action("USRS", "users"));
    let result = rewriter.finish();
    assert_eq!(result, "SELECT * FROM users");
}

#[test]
fn rewriter_whole_word_matching_no_partial() {
    // "user" should not be replaced inside "user_id"
    let mut rewriter = SqlRewriter::new("SELECT user_id FROM user");
    rewriter.apply(&make_fix_action("user", "users"));
    let result = rewriter.finish();
    assert_eq!(result, "SELECT user_id FROM users");
}

#[test]
fn rewriter_no_match_returns_original() {
    let mut rewriter = SqlRewriter::new("SELECT * FROM users");
    rewriter.apply(&make_fix_action("nonexistent", "replacement"));
    let result = rewriter.finish();
    assert_eq!(result, "SELECT * FROM users");
}

#[test]
fn rewriter_empty_replacements_returns_original() {
    let rewriter = SqlRewriter::new("SELECT * FROM users");
    let result = rewriter.finish();
    assert_eq!(result, "SELECT * FROM users");
}

#[test]
fn rewriter_with_source_location() {
    let action = FixAction {
        action: FixActionType::ReplaceTable,
        original: "usrs".to_string(),
        replacement: "users".to_string(),
        confidence: 0.95,
        explanation: "test".to_string(),
        location: Some(SourceLocation {
            line: 1,
            column: 15,
        }),
    };
    let mut rewriter = SqlRewriter::new("SELECT * FROM usrs");
    rewriter.apply(&action);
    let result = rewriter.finish();
    assert_eq!(result, "SELECT * FROM users");
}

#[test]
fn rewriter_multiline_sql() {
    let sql = "SELECT *\nFROM usrs\nWHERE id = 1";
    let mut rewriter = SqlRewriter::new(sql);
    rewriter.apply(&make_fix_action("usrs", "users"));
    let result = rewriter.finish();
    assert_eq!(result, "SELECT *\nFROM users\nWHERE id = 1");
}

// ─── Integration Tests (using fix engine + ecommerce schema) ────────

#[tokio::test]
async fn fix_table_typo_fixture() {
    let schema = ecommerce_schema();
    let sql = load_fixer_fixture("table_typo.sql");
    let result = fix_default(&sql, &schema).await;

    assert!(result.fixed, "Expected fix to be applied");
    assert!(result.post_fix_valid, "Expected fixed SQL to be valid");
    assert!(!result.fixes_applied.is_empty());
    assert_eq!(result.fixes_applied[0].original, "user");
    assert_eq!(result.fixes_applied[0].replacement, "users");
    assert!(result.fixed_sql.contains("users"));
}

#[tokio::test]
async fn fix_column_typo_fixture() {
    let schema = ecommerce_schema();
    let sql = load_fixer_fixture("column_typo.sql");
    let result = fix_default(&sql, &schema).await;

    assert!(result.fixed, "Expected fix to be applied");
    assert!(result.post_fix_valid, "Expected fixed SQL to be valid");
    assert!(!result.fixes_applied.is_empty());
    assert_eq!(result.fixes_applied[0].original, "created_date");
    assert_eq!(result.fixes_applied[0].replacement, "created_at");
}

#[tokio::test]
async fn fix_multiple_errors_fixture() {
    let schema = ecommerce_schema();
    let sql = load_fixer_fixture("multiple_errors.sql");
    let result = fix_default(&sql, &schema).await;

    assert!(result.fixed, "Expected fixes to be applied");
    // Should fix both the table and column errors
    assert!(
        result.fixes_applied.len() >= 1,
        "Expected at least 1 fix, got {}",
        result.fixes_applied.len()
    );
    // The fixed SQL should be valid (both errors fixed)
    assert!(
        result.post_fix_valid,
        "Expected fixed SQL to pass validation, fixed_sql: {}",
        result.fixed_sql
    );
}

#[tokio::test]
async fn fix_already_valid_fixture() {
    let schema = ecommerce_schema();
    let sql = load_fixer_fixture("already_valid.sql");
    let result = fix_default(&sql, &schema).await;

    assert!(!result.fixed, "Expected no fixes for valid SQL");
    assert!(result.fixes_applied.is_empty());
    assert!(result.post_fix_valid);
    assert!(result.unfixable_errors.is_empty());
}

#[tokio::test]
async fn fix_unfixable_syntax_fixture() {
    let schema = ecommerce_schema();
    let sql = load_fixer_fixture("unfixable_syntax.sql");
    let result = fix_default(&sql, &schema).await;

    assert!(!result.fixed, "Syntax errors should not be fixable");
    assert!(!result.post_fix_valid);
    assert!(
        !result.unfixable_errors.is_empty(),
        "Expected unfixable errors"
    );
}

#[tokio::test]
async fn fix_unfixable_no_match_fixture() {
    let schema = ecommerce_schema();
    let sql = load_fixer_fixture("unfixable_no_match.sql");
    let result = fix_default(&sql, &schema).await;

    assert!(
        !result.fixed || !result.post_fix_valid,
        "Gibberish table name should not be fixable"
    );
}

#[tokio::test]
async fn fix_cascading_fixture() {
    let schema = ecommerce_schema();
    let sql = load_fixer_fixture("cascading.sql");
    let result = fix_default(&sql, &schema).await;

    // "SELECT total FROM user" — "user" -> "users" (table fix), then
    // "total" might need fixing too (to "total_amount" in orders, but
    // now the table is "users" which doesn't have "total").
    // The key thing is that the engine tries multiple iterations.
    assert!(result.iterations >= 1, "Expected at least 1 iteration");
    assert!(result.fixed, "Expected some fix to be applied");
}

#[tokio::test]
async fn fix_with_high_confidence_threshold_skips_fixes() {
    let schema = ecommerce_schema();
    let config = FixConfig {
        min_confidence: 1.0, // Nothing is 100% confident
        max_iterations: 3,
        revalidate: true,
    };
    let result = fix("SELECT * FROM usrs", &schema, &config).await;
    // With confidence threshold of 1.0, "usrs" -> "users" likely won't pass
    // (Jaro-Winkler for "usrs"/"users" is ~0.93)
    assert!(
        !result.fixed || !result.post_fix_valid,
        "Very high threshold should prevent fixes"
    );
}

#[tokio::test]
async fn fix_with_max_iterations_one() {
    let schema = ecommerce_schema();
    let config = FixConfig {
        min_confidence: 0.8,
        max_iterations: 1,
        revalidate: true,
    };
    let result = fix("SELECT * FROM usrs", &schema, &config).await;
    assert!(
        result.iterations <= 2,
        "Should not exceed 1 iteration plus possible revalidation"
    );
}

#[tokio::test]
async fn fix_result_serialization_roundtrip() {
    let schema = ecommerce_schema();
    let result = fix_default("SELECT * FROM usrs", &schema).await;

    let json = serde_json::to_string(&result).expect("FixResult should serialize");
    let deserialized: FixResult =
        serde_json::from_str(&json).expect("FixResult should deserialize");

    assert_eq!(result.fixed, deserialized.fixed);
    assert_eq!(result.fixed_sql, deserialized.fixed_sql);
    assert_eq!(result.original_sql, deserialized.original_sql);
    assert_eq!(result.post_fix_valid, deserialized.post_fix_valid);
    assert_eq!(result.fixes_applied.len(), deserialized.fixes_applied.len());
    assert_eq!(result.iterations, deserialized.iterations);
}

#[tokio::test]
async fn fix_preserves_original_sql() {
    let schema = ecommerce_schema();
    let original = "SELECT * FROM usrs";
    let result = fix_default(original, &schema).await;

    assert_eq!(result.original_sql, original);
    assert_ne!(result.fixed_sql, original);
}

#[tokio::test]
async fn fix_valid_complex_query_unchanged() {
    let schema = ecommerce_schema();
    let sql = r#"
        SELECT u.name, o.total_amount
        FROM users u
        JOIN orders o ON u.id = o.user_id
        WHERE o.status = 'completed'
    "#;
    let result = fix_default(sql, &schema).await;
    assert!(!result.fixed);
    assert!(result.post_fix_valid);
    assert_eq!(result.fixed_sql, sql);
}

#[tokio::test]
async fn fix_action_has_correct_action_type_for_table() {
    let schema = ecommerce_schema();
    let result = fix_default("SELECT * FROM usrs", &schema).await;
    assert!(result.fixed);
    let fix_action = &result.fixes_applied[0];
    assert!(matches!(fix_action.action, FixActionType::ReplaceTable));
}

#[tokio::test]
async fn fix_action_has_correct_action_type_for_column() {
    let schema = ecommerce_schema();
    let result = fix_default("SELECT created_date FROM users", &schema).await;
    assert!(result.fixed);
    let fix_action = &result.fixes_applied[0];
    assert!(matches!(fix_action.action, FixActionType::ReplaceColumn));
}

#[tokio::test]
async fn fix_action_confidence_in_valid_range() {
    let schema = ecommerce_schema();
    let result = fix_default("SELECT * FROM usrs", &schema).await;
    for action in &result.fixes_applied {
        assert!(
            action.confidence >= 0.0 && action.confidence <= 1.0,
            "Fix confidence {} out of range",
            action.confidence
        );
        assert!(
            action.confidence >= 0.8,
            "Fix confidence {} below threshold",
            action.confidence
        );
    }
}

#[tokio::test]
async fn fix_action_explanation_is_nonempty() {
    let schema = ecommerce_schema();
    let result = fix_default("SELECT * FROM usrs", &schema).await;
    for action in &result.fixes_applied {
        assert!(
            !action.explanation.is_empty(),
            "Fix explanation should not be empty"
        );
    }
}

#[tokio::test]
async fn fix_time_ms_is_recorded() {
    let schema = ecommerce_schema();
    let result = fix_default("SELECT * FROM usrs", &schema).await;
    // We just check the field exists and is reasonable
    assert!(
        result.fix_time_ms < 30_000,
        "Fix should complete in under 30 seconds"
    );
}

#[tokio::test]
async fn fix_default_uses_standard_config() {
    let schema = ecommerce_schema();
    let result1 = fix_default("SELECT * FROM usrs", &schema).await;
    let result2 = fix("SELECT * FROM usrs", &schema, &FixConfig::default()).await;

    assert_eq!(result1.fixed, result2.fixed);
    assert_eq!(result1.fixed_sql, result2.fixed_sql);
}

#[tokio::test]
async fn fix_with_revalidate_false() {
    let schema = ecommerce_schema();
    let config = FixConfig {
        min_confidence: 0.8,
        max_iterations: 3,
        revalidate: false,
    };
    let result = fix("SELECT * FROM usrs", &schema, &config).await;
    // The engine validates within the loop, so if the fix succeeds
    // in-loop, post_fix_valid is true regardless of the revalidate flag.
    // The revalidate flag only affects the post-loop final check.
    assert!(result.fixed);
    assert_eq!(result.fixed_sql, "SELECT * FROM users");
}

#[tokio::test]
async fn fix_empty_sql() {
    let schema = ecommerce_schema();
    let result = fix_default("", &schema).await;
    assert!(!result.post_fix_valid);
}

#[tokio::test]
async fn fix_singular_table_name() {
    let schema = ecommerce_schema();
    // "order" -> "orders" (singular to plural)
    let result = fix_default("SELECT * FROM order", &schema).await;
    assert!(result.fixed);
    assert!(
        result.fixed_sql.contains("orders"),
        "Expected 'order' to be fixed to 'orders', got: {}",
        result.fixed_sql
    );
    assert!(result.post_fix_valid);
}

#[tokio::test]
async fn fix_product_singular() {
    let schema = ecommerce_schema();
    let result = fix_default("SELECT * FROM product", &schema).await;
    assert!(result.fixed);
    assert!(
        result.fixed_sql.contains("products"),
        "Expected 'product' to be fixed to 'products', got: {}",
        result.fixed_sql
    );
    assert!(result.post_fix_valid);
}

// ─── Snapshot Tests ─────────────────────────────────────────────────

#[tokio::test]
async fn snapshot_fix_result_table_typo() {
    let schema = ecommerce_schema();
    let result = fix_default("SELECT * FROM user", &schema).await;
    let snapshot = serde_json::json!({
        "fixed": result.fixed,
        "post_fix_valid": result.post_fix_valid,
        "fixes_count": result.fixes_applied.len(),
        "original_sql": result.original_sql,
        "fixed_sql": result.fixed_sql,
    });
    insta::assert_yaml_snapshot!("fix_result_table_typo", snapshot);
}

#[tokio::test]
async fn snapshot_fix_result_multiple_errors() {
    let schema = ecommerce_schema();
    let sql = load_fixer_fixture("multiple_errors.sql");
    let result = fix_default(&sql, &schema).await;
    let snapshot = serde_json::json!({
        "fixed": result.fixed,
        "post_fix_valid": result.post_fix_valid,
        "fixes_count": result.fixes_applied.len(),
        "iterations": result.iterations,
    });
    insta::assert_yaml_snapshot!("fix_result_multiple_errors", snapshot);
}

// ─── FixConfig Tests ────────────────────────────────────────────────

#[test]
fn fix_config_default_values() {
    let config = FixConfig::default();
    assert!((config.min_confidence - 0.8).abs() < f64::EPSILON);
    assert_eq!(config.max_iterations, 3);
    assert!(config.revalidate);
}

#[test]
fn fix_config_serialization_roundtrip() {
    let config = FixConfig {
        min_confidence: 0.75,
        max_iterations: 5,
        revalidate: false,
    };
    let json = serde_json::to_string(&config).expect("FixConfig should serialize");
    let deserialized: FixConfig =
        serde_json::from_str(&json).expect("FixConfig should deserialize");
    assert!((deserialized.min_confidence - 0.75).abs() < f64::EPSILON);
    assert_eq!(deserialized.max_iterations, 5);
    assert!(!deserialized.revalidate);
}

// ─── FixAction Type Tests ───────────────────────────────────────────

#[test]
fn fix_action_type_serialization() {
    let types = vec![
        FixActionType::ReplaceTable,
        FixActionType::ReplaceColumn,
        FixActionType::QualifyColumn,
        FixActionType::RemoveAlias,
        FixActionType::AddAlias,
        FixActionType::UseAlternative,
    ];
    for action_type in types {
        let json = serde_json::to_string(&action_type).expect("Should serialize");
        let _: FixActionType = serde_json::from_str(&json).expect("Should deserialize");
    }
}

#[test]
fn fix_action_type_snake_case_json() {
    let json = serde_json::to_string(&FixActionType::ReplaceTable).unwrap();
    assert_eq!(json, "\"replace_table\"");

    let json = serde_json::to_string(&FixActionType::ReplaceColumn).unwrap();
    assert_eq!(json, "\"replace_column\"");

    let json = serde_json::to_string(&FixActionType::QualifyColumn).unwrap();
    assert_eq!(json, "\"qualify_column\"");
}
