use super::*;

#[test]
fn builder_ephemeral_succeeds() {
    let db = TidalDb::builder().ephemeral().open();
    assert!(db.is_ok());
}

#[test]
fn builder_default_is_ephemeral() {
    let db = TidalDb::builder().open();
    assert!(db.is_ok());
}

#[test]
fn builder_persistent_requires_data_dir() {
    // Construct a persistent-mode builder without calling with_data_dir
    // by manually setting mode.
    let builder = TidalDbBuilder {
        config: Config {
            mode: StorageMode::Persistent,
            data_dir: None,
            wal_dir: None,
            cache_dir: None,
            cluster: Default::default(),
        },
        #[cfg(feature = "metrics")]
        metrics_addr: None,
        schema: None,
        rate_limiter_config: None,
        transport: None,
        profiles: Vec::new(),
    };
    let result = builder.validate();
    assert!(result.is_err());
    let err = result.expect_err("should fail");
    assert!(
        matches!(err, ConfigError::MissingDataDir),
        "expected MissingDataDir, got: {err}"
    );
}

#[test]
fn builder_persistent_missing_dir() {
    let result = TidalDb::builder()
        .with_data_dir("/nonexistent/path/that/does/not/exist")
        .open();
    assert!(result.is_err());
    let err_msg = result.expect_err("should fail").to_string();
    assert!(
        err_msg.contains("does not exist"),
        "expected DirectoryNotFound, got: {err_msg}"
    );
}

#[test]
fn builder_persistent_existing_dir() {
    let tmp = tempfile::tempdir().expect("failed to create tempdir");
    let result = TidalDb::builder().with_data_dir(tmp.path()).open();
    assert!(result.is_ok(), "open with valid tempdir should succeed");
}

#[test]
fn health_check_ok() {
    let db = TidalDb::builder().ephemeral().open().expect("open failed");
    assert!(db.health_check().is_ok());
}

#[test]
fn close_ok() {
    let db = TidalDb::builder().ephemeral().open().expect("open failed");
    assert!(db.close().is_ok());
}

#[test]
fn builder_with_wal_and_cache_dir() {
    let tmp = tempfile::tempdir().expect("failed to create tempdir");
    let wal = tmp.path().join("wal");
    let cache = tmp.path().join("cache");
    std::fs::create_dir_all(&wal).expect("mkdir wal");
    std::fs::create_dir_all(&cache).expect("mkdir cache");

    let result = TidalDb::builder()
        .with_data_dir(tmp.path())
        .wal_dir(&wal)
        .cache_dir(&cache)
        .open();
    assert!(
        result.is_ok(),
        "open with explicit wal/cache dirs should succeed"
    );
}

#[test]
fn wal_dir_override_relocates_the_wal() {
    // W1 regression: an explicit wal_dir override OUTSIDE {data_dir}/wal must be
    // honored — the WAL segment must be written at the override path, NOT at the
    // {data_dir}/wal default. Proves the override flows to the writer (no
    // silent-ignore) and that writer/readers resolve the same directory.
    use std::time::Duration;

    use crate::schema::{DecaySpec, EntityId, EntityKind, SchemaBuilder, Timestamp, Window};

    let data = tempfile::tempdir().expect("data tempdir");
    let walroot = tempfile::tempdir().expect("wal tempdir");
    let custom_wal = walroot.path().join("custom_wal");
    std::fs::create_dir_all(&custom_wal).expect("mkdir custom wal");

    let mut sb = SchemaBuilder::new();
    let _ = sb
        .signal(
            "view",
            EntityKind::Item,
            DecaySpec::Exponential {
                half_life: Duration::from_secs(3600),
            },
        )
        .windows(&[Window::AllTime])
        .velocity(false)
        .add();
    let schema = sb.build().expect("valid schema");

    {
        let db = TidalDb::builder()
            .with_data_dir(data.path())
            .wal_dir(&custom_wal)
            .with_schema(schema)
            .open()
            .expect("open with relocated wal_dir should succeed");
        // A durable signal write forces a WAL segment to be created.
        db.signal("view", EntityId::new(1), 1.0, Timestamp::now())
            .expect("signal write");
        db.close().expect("clean close");
    }

    // WAL artifacts must appear under the override, and NOT under {data_dir}/wal.
    let override_has_files = std::fs::read_dir(&custom_wal)
        .map(|d| d.flatten().any(|e| e.path().is_file()))
        .unwrap_or(false);
    assert!(
        override_has_files,
        "WAL artifacts must appear under the wal_dir override at {custom_wal:?}"
    );
    let default_wal = data.path().join("wal");
    let default_has_files = std::fs::read_dir(&default_wal)
        .map(|d| d.flatten().any(|e| e.path().is_file()))
        .unwrap_or(false);
    assert!(
        !default_has_files,
        "no WAL artifacts may appear at the default {{data_dir}}/wal when overridden"
    );
}

#[test]
fn builder_ephemeral_resets_dirs() {
    let builder = TidalDb::builder()
        .with_data_dir("/some/path")
        .wal_dir("/some/wal")
        .cache_dir("/some/cache")
        .ephemeral();

    assert_eq!(builder.config.mode, StorageMode::Ephemeral);
    assert!(builder.config.data_dir.is_none());
    assert!(builder.config.wal_dir.is_none());
    assert!(builder.config.cache_dir.is_none());
}

#[test]
fn builder_wal_dir_nonexistent() {
    let tmp = tempfile::tempdir().expect("failed to create tempdir");
    let result = TidalDb::builder()
        .with_data_dir(tmp.path())
        .wal_dir("/nonexistent/wal")
        .open();
    assert!(result.is_err());
    let err_msg = result.expect_err("should fail").to_string();
    assert!(err_msg.contains("does not exist"));
}

#[test]
fn resolve_defaults_sets_wal_and_cache() {
    let tmp = tempfile::tempdir().expect("failed to create tempdir");
    let mut builder = TidalDb::builder().with_data_dir(tmp.path());
    assert!(builder.config.wal_dir.is_none());
    assert!(builder.config.cache_dir.is_none());

    builder.resolve_defaults();

    let paths = Paths::new(tmp.path());
    assert_eq!(builder.config.wal_dir.as_ref(), Some(&paths.wal_dir()));
    assert_eq!(builder.config.cache_dir.as_ref(), Some(&paths.cache_dir()));
}

#[test]
fn resolve_defaults_preserves_explicit_overrides() {
    let tmp = tempfile::tempdir().expect("failed to create tempdir");
    let custom_wal = tmp.path().join("custom_wal");
    let custom_cache = tmp.path().join("custom_cache");

    let mut builder = TidalDb::builder()
        .with_data_dir(tmp.path())
        .wal_dir(&custom_wal)
        .cache_dir(&custom_cache);

    builder.resolve_defaults();

    assert_eq!(builder.config.wal_dir.as_ref(), Some(&custom_wal));
    assert_eq!(builder.config.cache_dir.as_ref(), Some(&custom_cache));
}

#[test]
fn resolve_defaults_noop_for_ephemeral() {
    let mut builder = TidalDb::builder().ephemeral();
    builder.resolve_defaults();

    assert!(builder.config.wal_dir.is_none());
    assert!(builder.config.cache_dir.is_none());
}

// ── Fix A: Directory lock tests ─────────────────────────────────────

#[test]
fn dual_open_same_directory_fails() {
    let dir = tempfile::tempdir().expect("tempdir");
    // First open succeeds (no schema -- M0 mode, but still persistent).
    let _db1 = TidalDb::builder()
        .with_data_dir(dir.path())
        .open()
        .expect("first open should succeed");

    // Second open on the same directory must fail with DataDirLocked.
    let result = TidalDb::builder().with_data_dir(dir.path()).open();
    assert!(result.is_err(), "expected error for dual open");
    let err_msg = result.expect_err("should fail").to_string();
    assert!(
        err_msg.contains("already open"),
        "expected DataDirLocked, got: {err_msg}"
    );
}

#[test]
fn lock_released_after_close() {
    let dir = tempfile::tempdir().expect("tempdir");
    {
        let db = TidalDb::builder()
            .with_data_dir(dir.path())
            .open()
            .expect("open");
        db.close().expect("close");
    }
    // After close (and drop), the lock should be released.
    let db2 = TidalDb::builder().with_data_dir(dir.path()).open();
    assert!(
        db2.is_ok(),
        "second open after close should succeed: {:?}",
        db2.err()
    );
}

#[test]
fn ephemeral_mode_skips_lock() {
    // Two ephemeral databases should both succeed (no lock file).
    let _db1 = TidalDb::builder().ephemeral().open().expect("open 1");
    let _db2 = TidalDb::builder().ephemeral().open().expect("open 2");
}

// ── CONCURRENCY-1: session-TTL sweeper actually runs in persistent mode ──────

#[test]
fn open_arc_persistent_starts_live_sweeper() {
    // The sweeper holds a Weak<TidalDb> whose upgrade target is the Arc that
    // `open_arc` returns. Prior to the fix, `open()` tried to start it against a
    // temporary Arc that was immediately unwrapped, so the thread was
    // dead-on-arrival in every persistent deployment. Prove the thread is live
    // after a persistent `open_arc`.
    let dir = tempfile::tempdir().expect("tempdir");
    let db = TidalDb::builder()
        .with_data_dir(dir.path())
        .open_arc()
        .expect("persistent open_arc should succeed");

    assert!(
        db.sweeper_is_running(),
        "persistent open_arc must start a live session-TTL sweeper thread"
    );

    // Dropping the only Arc runs `Drop` → `shutdown_inner`, which signals the
    // sweeper and joins it (the by-value `close()` cannot be called through an
    // `Arc`). This is the real shutdown path for an `open_arc` caller.
    drop(db);
}

#[test]
fn open_arc_ephemeral_skips_sweeper() {
    // Ephemeral sessions never persist, so no sweeper thread is needed.
    let db = TidalDb::builder()
        .ephemeral()
        .open_arc()
        .expect("ephemeral open_arc should succeed");
    assert!(
        !db.sweeper_is_running(),
        "ephemeral mode must not spawn a sweeper thread"
    );
}

#[test]
fn start_sweeper_on_long_lived_arc_runs() {
    // Mirrors how `tidal_server::state::ServerState::new` wires the sweeper:
    // wrap the db in the Arc the owner keeps, then call `start_sweeper` on it.
    let dir = tempfile::tempdir().expect("tempdir");
    let db = TidalDb::builder()
        .with_data_dir(dir.path())
        .open()
        .expect("persistent open should succeed");
    let db = std::sync::Arc::new(db);
    TidalDb::start_sweeper(&db);
    assert!(
        db.sweeper_is_running(),
        "start_sweeper against the long-lived Arc must spawn a live thread"
    );
}

// ── obs-REPL-1 / HEALTH-1: control plane installed in the persistent path ────

/// Build a minimal schema so the persistent (schema) branch of `open()` runs
/// `from_parts` — the path where the control-plane install ordering matters.
#[cfg(test)]
fn minimal_schema() -> crate::schema::Schema {
    use std::time::Duration;

    use crate::schema::{DecaySpec, EntityKind, SchemaBuilder, Window};

    let mut builder = SchemaBuilder::new();
    let _ = builder
        .signal(
            "view",
            EntityKind::Item,
            DecaySpec::Exponential {
                half_life: Duration::from_secs(3600),
            },
        )
        .windows(&[Window::AllTime])
        .velocity(false)
        .add();
    builder.build().expect("schema must be valid")
}

#[test]
fn persistent_open_installs_control_plane() {
    // Defect 2 (obs-REPL-1 part A): `from_parts` must install the control plane
    // into `MetricsState` while the metrics Arc is still uniquely owned. The
    // checkpoint thread clones the metrics Arc, so the install was previously
    // ordered AFTER those clones and `Arc::get_mut` silently returned `None`,
    // leaving the control plane un-wired in every persistent deployment. After
    // the fix `cluster_health()` is `Some` (the control plane is reachable from
    // the metrics state) on the real persistent-with-schema open path.
    let dir = tempfile::tempdir().expect("tempdir");
    let db = TidalDb::builder()
        .with_data_dir(dir.path())
        .with_schema(minimal_schema())
        .open()
        .expect("persistent open with schema should succeed");

    assert!(
        db.metrics().cluster_health().is_some(),
        "control plane must be installed on MetricsState in the persistent path"
    );

    db.close().expect("close should succeed");
}

#[cfg(feature = "metrics")]
#[test]
fn persistent_open_replication_lag_flows_through_control_plane() {
    // Defect 2 + 3 confirmation: with the control plane installed, the metrics
    // render reads replication lag from the live control-plane gauge. A freshly
    // opened single-node follower with no segments behind reports lag 0, and
    // the gauge is the SAME one the receiver feeds (proved in the receiver
    // unit test). This asserts the value is actually sourced from the control
    // plane rather than the un-wired fallback.
    let dir = tempfile::tempdir().expect("tempdir");
    let db = TidalDb::builder()
        .with_data_dir(dir.path())
        .with_schema(minimal_schema())
        .open()
        .expect("persistent open with schema should succeed");

    let prom = db.metrics().render_prometheus();
    assert!(
        prom.contains("tidaldb_replication_lag_seqno"),
        "replication lag metric must render: {prom}"
    );
    // The control plane is wired, so lag is sourced from the live gauge (0 when
    // caught up) rather than left un-wired.
    assert!(
        db.metrics().cluster_health().is_some(),
        "control plane must back the replication-lag reader"
    );

    db.close().expect("close should succeed");
}
