use mongreldb_core::{
    database::VTAB_DIR, ColumnDef, ColumnFlags, Database, ExternalTableDefinition,
    ExternalTableEntry, ModuleArg, ModuleCapabilities, Schema, StoredTrigger, TriggerCell,
    TriggerCondition, TriggerDefinition, TriggerEvent, TriggerProgram, TriggerStep, TriggerTarget,
    TriggerTiming, TriggerValue, TypeId, Value,
};
use tempfile::tempdir;

fn base_schema() -> Schema {
    Schema {
        schema_id: 1,
        columns: vec![ColumnDef {
            id: 1,
            name: "id".into(),
            ty: TypeId::Int64,
            flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
            default_value: None,
        }],
        indexes: Vec::new(),
        colocation: Vec::new(),
        constraints: Default::default(),
        clustered: false,
    }
}

fn external_schema() -> Schema {
    Schema {
        schema_id: 0,
        columns: vec![ColumnDef {
            id: 1,
            name: "value".into(),
            ty: TypeId::Int64,
            flags: ColumnFlags::empty(),
            default_value: None,
        }],
        indexes: Vec::new(),
        colocation: Vec::new(),
        constraints: Default::default(),
        clustered: false,
    }
}

fn external_entry(name: &str, trigger_safe: bool) -> ExternalTableEntry {
    ExternalTableEntry::new(
        name,
        ExternalTableDefinition {
            module: "series".into(),
            args: vec![ModuleArg::Number("3".into())],
            declared_schema: external_schema(),
            hidden_columns: Vec::new(),
            options: Default::default(),
            capabilities: ModuleCapabilities {
                read_only: true,
                deterministic: true,
                trigger_safe,
                ..ModuleCapabilities::default()
            },
        },
        0,
    )
    .unwrap()
}

fn writable_external_entry(name: &str) -> ExternalTableEntry {
    ExternalTableEntry::new(
        name,
        ExternalTableDefinition {
            module: "writable_ext".into(),
            args: Vec::new(),
            declared_schema: external_schema(),
            hidden_columns: Vec::new(),
            options: Default::default(),
            capabilities: ModuleCapabilities {
                writable: true,
                trigger_safe: true,
                transaction_safe: true,
                ..ModuleCapabilities::default()
            },
        },
        0,
    )
    .unwrap()
}

fn select_external_trigger(name: &str, source: &str) -> StoredTrigger {
    StoredTrigger::new(
        name,
        TriggerDefinition {
            target: TriggerTarget::Table("base".into()),
            timing: TriggerTiming::After,
            event: TriggerEvent::Insert,
            update_of: Vec::new(),
            target_columns: Vec::new(),
            when: None,
            program: TriggerProgram {
                steps: vec![TriggerStep::Select {
                    id: "probe".into(),
                    table: source.into(),
                    conditions: vec![TriggerCondition::IsNotNull { column_id: 1 }],
                }],
            },
        },
        0,
    )
    .unwrap()
}

fn insert_external_trigger(name: &str, target: &str) -> StoredTrigger {
    StoredTrigger::new(
        name,
        TriggerDefinition {
            target: TriggerTarget::Table("base".into()),
            timing: TriggerTiming::After,
            event: TriggerEvent::Insert,
            update_of: Vec::new(),
            target_columns: Vec::new(),
            when: None,
            program: TriggerProgram {
                steps: vec![TriggerStep::Insert {
                    table: target.into(),
                    cells: vec![TriggerCell {
                        column_id: 1,
                        value: TriggerValue::NewColumn(1),
                    }],
                }],
            },
        },
        0,
    )
    .unwrap()
}

#[test]
fn trigger_safe_external_tables_are_valid_trigger_sources() {
    let dir = tempdir().unwrap();
    let db = Database::create(dir.path()).unwrap();
    db.create_table("base", base_schema()).unwrap();
    db.create_external_table(external_entry("ext", true))
        .unwrap();

    let trigger = db
        .create_trigger(select_external_trigger("base_ai", "ext"))
        .unwrap();
    assert_eq!(trigger.name, "base_ai");
}

#[test]
fn non_trigger_safe_external_tables_are_rejected_as_trigger_sources() {
    let dir = tempdir().unwrap();
    let db = Database::create(dir.path()).unwrap();
    db.create_table("base", base_schema()).unwrap();
    db.create_external_table(external_entry("ext", false))
        .unwrap();

    let err = db
        .create_trigger(select_external_trigger("base_ai", "ext"))
        .unwrap_err();
    let msg = err.to_string();
    assert!(msg.contains("external table"), "{msg}");
    assert!(msg.contains("not trigger-safe"), "{msg}");
}

#[test]
fn plain_core_transactions_require_external_trigger_bridge_for_external_trigger_targets() {
    let dir = tempdir().unwrap();
    let db = Database::create(dir.path()).unwrap();
    db.create_table("base", base_schema()).unwrap();
    db.create_external_table(writable_external_entry("ext"))
        .unwrap();
    db.create_trigger(insert_external_trigger("base_ai", "ext"))
        .unwrap();

    let err = db
        .transaction(|tx| tx.put("base", vec![(1, Value::Int64(7))]).map(|_| ()))
        .unwrap_err();
    let msg = err.to_string();
    assert!(msg.contains("external table"), "{msg}");
    assert!(msg.contains("external trigger bridge"), "{msg}");
}

#[test]
fn orphan_external_table_state_is_reported_and_reclaimed_by_gc() {
    let dir = tempdir().unwrap();
    let db = Database::create(dir.path()).unwrap();
    db.create_external_table(external_entry("ext", true))
        .unwrap();

    let vtab_dir = db.root().join(VTAB_DIR);
    std::fs::create_dir_all(vtab_dir.join("ext")).unwrap();
    std::fs::create_dir_all(vtab_dir.join("orphan")).unwrap();

    let issues = db.check();
    assert!(
        issues.iter().any(|issue| issue.severity == "warning"
            && issue.table_name == "orphan"
            && issue.description.contains("orphan external table state")),
        "{issues:?}"
    );

    let reclaimed = db.gc().unwrap();
    assert!(reclaimed >= 1);
    assert!(vtab_dir.join("ext").exists());
    assert!(!vtab_dir.join("orphan").exists());
}
