#![expect(
    clippy::unwrap_used,
    reason = "SQLite secret-store integration tests use panic-on-failure fixture setup"
)]

use std::collections::HashMap;

use chrono::{TimeZone as _, Utc};
use fabro_db::Database;
use fabro_types::SecretType;
use fabro_vault::{SecretEntry, SecretStore, SecretStoreError, import_legacy_json_once};
use tokio::fs;

async fn test_database() -> (tempfile::TempDir, Database) {
    let dir = tempfile::tempdir().unwrap();
    let database = Database::connect(dir.path().join("fabro.sqlite3"))
        .await
        .unwrap();
    database.migrate().await.unwrap();
    (dir, database)
}

fn oauth_credential(access_token: &str) -> String {
    serde_json::json!({
        "tokens": {
            "access_token": access_token,
            "refresh_token": "refresh",
            "expires_at": "2026-07-11T13:00:00Z"
        },
        "config": {
            "auth_url": "https://auth.example.com",
            "token_url": "https://auth.example.com/token",
            "client_id": "client",
            "scopes": ["openid"],
            "redirect_uri": null,
            "use_pkce": true
        }
    })
    .to_string()
}

#[tokio::test]
async fn crud_preserves_metadata_and_description() {
    let (_dir, database) = test_database().await;
    let store = SecretStore::new(database.clone_pool());

    let created = store
        .set(
            "OPENAI_API_KEY",
            "first",
            SecretType::Token,
            Some("provider key"),
        )
        .await
        .unwrap();
    let updated = store
        .set("OPENAI_API_KEY", "second", SecretType::Token, None)
        .await
        .unwrap();

    assert_eq!(created.created_at, updated.created_at);
    assert_eq!(updated.description.as_deref(), Some("provider key"));
    let entry = store.get("OPENAI_API_KEY").await.unwrap().unwrap();
    assert_eq!(entry.value, "second");
    assert_eq!(entry.revision, 2);
    let listed = store.list().await.unwrap();
    assert_eq!(listed.len(), 1);
    assert_eq!(listed[0].name, updated.name);
    assert_eq!(listed[0].description, updated.description);

    store.remove("OPENAI_API_KEY").await.unwrap();
    assert!(store.get("OPENAI_API_KEY").await.unwrap().is_none());
}

#[tokio::test]
async fn independent_stores_observe_writes() {
    let (_dir, database) = test_database().await;
    let first = SecretStore::new(database.clone_pool());
    let second = SecretStore::new(database.clone_pool());

    first
        .set("ANTHROPIC_API_KEY", "key", SecretType::Token, None)
        .await
        .unwrap();

    assert_eq!(
        second
            .get("ANTHROPIC_API_KEY")
            .await
            .unwrap()
            .unwrap()
            .value,
        "key"
    );
}

#[tokio::test]
async fn replace_if_revision_rejects_stale_writer() {
    let (_dir, database) = test_database().await;
    let store = SecretStore::new(database.clone_pool());
    store
        .set(
            "OPENAI_CODEX",
            &oauth_credential("initial"),
            SecretType::Oauth,
            None,
        )
        .await
        .unwrap();

    let updated = store
        .replace_if_revision(
            "OPENAI_CODEX",
            1,
            &oauth_credential("winner"),
            SecretType::Oauth,
        )
        .await
        .unwrap();
    assert_eq!(updated.revision, 2);

    let err = store
        .replace_if_revision(
            "OPENAI_CODEX",
            1,
            &oauth_credential("loser"),
            SecretType::Oauth,
        )
        .await
        .unwrap_err();
    assert!(matches!(err, SecretStoreError::StaleRevision {
        expected: 1,
        actual: 2,
        ..
    }));
}

#[tokio::test]
async fn imports_legacy_json_once_without_overwriting_sql() {
    let (dir, database) = test_database().await;
    let store = SecretStore::new(database.clone_pool());
    store
        .set("EXISTING_KEY", "sql", SecretType::Token, None)
        .await
        .unwrap();
    let timestamp = Utc.with_ymd_and_hms(2026, 7, 11, 12, 0, 0).unwrap();
    let entries = HashMap::from([
        ("EXISTING_KEY".to_string(), SecretEntry {
            value:       "legacy".to_string(),
            secret_type: SecretType::Token,
            description: None,
            created_at:  timestamp,
            updated_at:  timestamp,
            revision:    1,
        }),
        ("NEW_KEY".to_string(), SecretEntry {
            value:       "new".to_string(),
            secret_type: SecretType::Token,
            description: Some("imported".to_string()),
            created_at:  timestamp,
            updated_at:  timestamp,
            revision:    1,
        }),
    ]);
    let source = dir.path().join("secrets.json");
    fs::write(&source, serde_json::to_vec(&entries).unwrap())
        .await
        .unwrap();

    let report = import_legacy_json_once(database.pool(), &source)
        .await
        .unwrap()
        .unwrap();

    assert_eq!(report.imported_rows, 1);
    assert_eq!(report.skipped_rows, 1);
    assert!(!source.exists());
    assert!(report.backup_path.exists());
    assert_eq!(
        store.get("EXISTING_KEY").await.unwrap().unwrap().value,
        "sql"
    );
    assert_eq!(store.get("NEW_KEY").await.unwrap().unwrap().value, "new");
    assert!(
        import_legacy_json_once(database.pool(), &source)
            .await
            .unwrap()
            .is_none()
    );
}

#[tokio::test]
async fn malformed_legacy_json_does_not_import_or_rename() {
    let (dir, database) = test_database().await;
    let source = dir.path().join("secrets.json");
    fs::write(&source, br#"{"VALID_KEY":{"value":"secret"}}"#)
        .await
        .unwrap();

    let err = import_legacy_json_once(database.pool(), &source)
        .await
        .unwrap_err();

    assert!(matches!(err, SecretStoreError::LegacyParse { .. }));
    assert!(source.exists());
    assert!(
        SecretStore::new(database.clone_pool())
            .list()
            .await
            .unwrap()
            .is_empty()
    );
}

#[tokio::test]
async fn github_private_key_file_secret_satisfies_schema() {
    let (_dir, database) = test_database().await;
    let store = SecretStore::new(database.clone_pool());

    store
        .set("GITHUB_APP_PRIVATE_KEY", "pem", SecretType::File, None)
        .await
        .unwrap();
    store
        .set("/run/secrets/key.pem", "pem", SecretType::File, None)
        .await
        .unwrap();
}

#[tokio::test]
async fn corrupted_stored_row_returns_typed_error() {
    let (_dir, database) = test_database().await;
    let mut connection = database.pool().acquire().await.unwrap();
    sqlx::query("PRAGMA ignore_check_constraints = ON")
        .execute(&mut *connection)
        .await
        .unwrap();
    sqlx::query(
        "INSERT INTO secrets (name, secret_type, value, revision, created_at, updated_at) \
         VALUES (?, ?, ?, ?, ?, ?)",
    )
    .bind("VALID_NAME")
    .bind("token")
    .bind("value")
    .bind(0_i64)
    .bind("2026-07-11T12:00:00Z")
    .bind("2026-07-11T12:00:00Z")
    .execute(&mut *connection)
    .await
    .unwrap();

    let err = SecretStore::new(database.clone_pool())
        .get("VALID_NAME")
        .await
        .unwrap_err();
    assert!(matches!(err, SecretStoreError::StoredRevision {
        revision: 0,
        ..
    }));
}

#[test]
fn debug_redacts_secret_value() {
    let timestamp = Utc.with_ymd_and_hms(2026, 7, 11, 12, 0, 0).unwrap();
    let entry = SecretEntry {
        value:       "do-not-print".to_string(),
        secret_type: SecretType::Token,
        description: None,
        created_at:  timestamp,
        updated_at:  timestamp,
        revision:    1,
    };

    let rendered = format!("{entry:?}");
    assert!(!rendered.contains("do-not-print"));
    assert!(rendered.contains("[REDACTED]"));
}
