use std::{fs, io::Write, path::Path, time::Duration};

use attune::{
    BackendError, ChangeEvent, ChangeSource, Settings, SettingsError, SqliteBackend,
    StorageBackend, StoredValue,
};

#[derive(Clone, Settings)]
struct PersistConfig {
    #[setting(default = 8080)]
    port: u16,

    #[setting(persist, default = "system")]
    theme: String,
}

#[derive(Clone, Settings)]
#[settings(app = "derive_persist_source_test")]
struct PersistFallbackConfig {
    #[setting(persist, env = "ATTUNE_DERIVE_PERSIST_THEME", default = "system")]
    theme: String,
}

#[derive(Clone, Settings)]
#[settings(
    app = "derive_persist_toml_test",
    config_path = "/tmp/attune-derive-persist.toml",
    db_path = "/tmp/attune-derive-persist.db"
)]
struct PersistTomlFallbackConfig {
    #[setting(persist, env = "ATTUNE_DERIVE_PERSIST_TOML_THEME", default = "system")]
    theme: String,
}

#[derive(Clone, Settings)]
#[settings(app = "derive_persist_db_test", db_path = "settings.db")]
struct PersistDbConfig {
    #[setting(persist, env = "ATTUNE_DERIVE_PERSIST_DB_THEME", default = "system")]
    theme: String,
}

#[derive(Clone, Settings)]
#[settings(app = "derive_persist_no_cross_process_test", cross_process = false)]
struct PersistNoCrossProcessConfig {
    #[setting(persist, default = "system")]
    theme: String,
}

#[derive(Clone, Settings)]
#[settings(app = "derive_persist_key_test")]
struct PersistKeyConfig {
    #[setting(persist, default = "info", key = "log.level")]
    log_level: String,
}

#[derive(Clone, Settings)]
#[settings(app = "derive_persist_deserialize_fallback_test")]
struct PersistDeserializeFallbackConfig {
    #[setting(persist, default = 80, deserialize_fallback = "parse_legacy_port")]
    port: u16,
}

#[derive(Clone, Settings)]
#[settings(app = "derive_persist_bad_db_test", db_path = "/tmp")]
#[allow(dead_code)]
struct PersistBadDbConfig {
    #[setting(persist, default = "system")]
    theme: String,
}

fn parse_legacy_port(raw: &str) -> Result<u16, String> {
    match raw {
        "\"legacy-port\"" | "legacy-port" => Ok(9000),
        other => Err(format!("unknown legacy port: {other}")),
    }
}

#[test]
fn persist_field_setter_updates_getter_and_snapshot() {
    let db = tempfile::NamedTempFile::new().unwrap();

    temp_env::with_var("ATTUNE_CONFIG", Some(db.path().to_str().unwrap()), || {
        let settings = PersistConfig::load().unwrap();

        settings.theme().set("dark".to_string()).unwrap();

        assert_eq!(settings.theme().get(), "dark");
        assert_eq!(settings.snapshot().theme, "dark");
        assert_eq!(settings.port().get(), 8080);
    });
}

#[test]
fn persist_field_setter_emits_local_change_event() {
    let db = tempfile::NamedTempFile::new().unwrap();

    temp_env::with_var("ATTUNE_CONFIG", Some(db.path().to_str().unwrap()), || {
        let settings = PersistConfig::load().unwrap();
        let rx = settings.on_change();

        settings.theme().set("dark".to_string()).unwrap();

        let event = rx.recv().unwrap();
        match event {
            ChangeEvent::Set {
                key,
                old_value,
                new_value,
                source,
                ..
            } => {
                assert_eq!(key, "theme");
                assert_eq!(source, ChangeSource::Local);
                assert_eq!(
                    old_value,
                    Some(StoredValue::encode(&"system".to_string()).unwrap())
                );
                assert_eq!(new_value, StoredValue::encode(&"dark".to_string()).unwrap());
            }
            event => panic!("unexpected event: {event:?}"),
        }
    });
}

#[test]
fn persist_field_loads_stored_value_before_env_toml_and_default() {
    let db = tempfile::NamedTempFile::new().unwrap();
    let backend = SqliteBackend::open(db.path()).unwrap();
    backend
        .set(
            "theme",
            &StoredValue::encode(&"stored".to_string()).unwrap(),
        )
        .unwrap();

    temp_env::with_vars(
        [
            (
                "DERIVE_PERSIST_DB_TEST_CONFIG",
                Some(db.path().to_str().unwrap()),
            ),
            ("ATTUNE_DERIVE_PERSIST_DB_THEME", Some("env")),
        ],
        || {
            let settings = PersistDbConfig::load().unwrap();

            assert_eq!(settings.theme().get(), "stored");
        },
    );
}

#[test]
fn persist_field_load_returns_error_when_sqlite_cannot_open() {
    let error = match PersistBadDbConfig::load() {
        Ok(_) => panic!("expected load to fail"),
        Err(error) => error,
    };

    match error {
        SettingsError::Backend(BackendError::Open(message)) => {
            assert!(!message.is_empty());
        }
        error => panic!("unexpected error: {error:?}"),
    }
}

#[test]
fn external_persist_field_write_updates_getter() {
    let db = tempfile::NamedTempFile::new().unwrap();

    temp_env::with_vars(
        [
            (
                "DERIVE_PERSIST_DB_TEST_CONFIG",
                Some(db.path().to_str().unwrap()),
            ),
            ("ATTUNE_DERIVE_PERSIST_DB_THEME", None),
        ],
        || {
            let settings = PersistDbConfig::load().unwrap();
            let rx = settings.on_change();
            let other = SqliteBackend::open(db.path()).unwrap();

            other
                .set(
                    "theme",
                    &StoredValue::encode(&"external".to_string()).unwrap(),
                )
                .unwrap();

            let event = rx.recv_timeout(Duration::from_secs(3)).unwrap();
            assert!(matches!(
                event,
                ChangeEvent::Set {
                    source: ChangeSource::External,
                    ..
                }
            ));
            assert_eq!(settings.theme().get(), "external");
        },
    );
}

#[test]
fn invalid_external_persist_field_write_preserves_getter() {
    let db = tempfile::NamedTempFile::new().unwrap();

    temp_env::with_vars(
        [
            (
                "DERIVE_PERSIST_DB_TEST_CONFIG",
                Some(db.path().to_str().unwrap()),
            ),
            ("ATTUNE_DERIVE_PERSIST_DB_THEME", None),
        ],
        || {
            let settings = PersistDbConfig::load().unwrap();
            let rx = settings.on_change();
            let other = SqliteBackend::open(db.path()).unwrap();

            other
                .set("theme", &StoredValue::from_raw("not-json".to_string()))
                .unwrap();

            let event = rx.recv_timeout(Duration::from_secs(3)).unwrap();
            match event {
                ChangeEvent::DeserializeFailure { key, raw, .. } => {
                    assert_eq!(key, "theme");
                    assert_eq!(raw, "not-json");
                }
                event => panic!("unexpected event: {event:?}"),
            }
            assert_eq!(settings.theme().get(), "system");
        },
    );
}

#[test]
fn persist_field_uses_env_fallback_when_no_value_is_stored() {
    let db = tempfile::NamedTempFile::new().unwrap();

    temp_env::with_vars(
        [
            (
                "DERIVE_PERSIST_SOURCE_TEST_CONFIG",
                Some(db.path().to_str().unwrap()),
            ),
            ("ATTUNE_DERIVE_PERSIST_THEME", Some("dark")),
        ],
        || {
            let settings = PersistFallbackConfig::load().unwrap();

            assert_eq!(settings.theme().get(), "dark");
        },
    );
}

#[test]
fn persist_field_uses_toml_fallback_when_env_and_stored_value_are_absent() {
    let config_path = Path::new("/tmp/attune-derive-persist.toml");
    let db_path = Path::new("/tmp/attune-derive-persist.db");
    let _ = fs::remove_file(config_path);
    let _ = fs::remove_file(db_path);

    let mut file = fs::File::create(config_path).unwrap();
    writeln!(file, "theme = 'dark'").unwrap();

    temp_env::with_vars([("ATTUNE_DERIVE_PERSIST_TOML_THEME", None::<&str>)], || {
        let settings = PersistTomlFallbackConfig::load().unwrap();

        assert_eq!(settings.theme().get(), "dark");
    });

    let _ = fs::remove_file(config_path);
    let _ = fs::remove_file(db_path);
}

#[test]
fn persist_field_uses_default_when_no_fallback_source_exists() {
    let db = tempfile::NamedTempFile::new().unwrap();

    temp_env::with_vars(
        [
            (
                "DERIVE_PERSIST_SOURCE_TEST_CONFIG",
                Some(db.path().to_str().unwrap()),
            ),
            ("ATTUNE_DERIVE_PERSIST_THEME", None),
        ],
        || {
            let settings = PersistFallbackConfig::load().unwrap();

            assert_eq!(settings.theme().get(), "system");
        },
    );
}

#[test]
fn persist_field_on_change_emits_local_typed_value() {
    let db = tempfile::NamedTempFile::new().unwrap();

    temp_env::with_var("ATTUNE_CONFIG", Some(db.path().to_str().unwrap()), || {
        let settings = PersistConfig::load().unwrap();
        let rx = settings.theme().on_change();

        settings.theme().set("dark".to_string()).unwrap();

        assert_eq!(rx.recv_timeout(Duration::from_secs(2)).unwrap(), "dark");
    });
}

#[test]
fn persist_field_on_change_emits_external_typed_value() {
    let db = tempfile::NamedTempFile::new().unwrap();

    temp_env::with_vars(
        [
            (
                "DERIVE_PERSIST_DB_TEST_CONFIG",
                Some(db.path().to_str().unwrap()),
            ),
            ("ATTUNE_DERIVE_PERSIST_DB_THEME", None),
        ],
        || {
            let settings = PersistDbConfig::load().unwrap();
            let rx = settings.theme().on_change();
            let other = SqliteBackend::open(db.path()).unwrap();

            other
                .set(
                    "theme",
                    &StoredValue::encode(&"external".to_string()).unwrap(),
                )
                .unwrap();

            assert_eq!(rx.recv_timeout(Duration::from_secs(3)).unwrap(), "external");
        },
    );
}

#[test]
fn persist_field_on_change_ignores_invalid_external_value() {
    let db = tempfile::NamedTempFile::new().unwrap();

    temp_env::with_vars(
        [
            (
                "DERIVE_PERSIST_DB_TEST_CONFIG",
                Some(db.path().to_str().unwrap()),
            ),
            ("ATTUNE_DERIVE_PERSIST_DB_THEME", None),
        ],
        || {
            let settings = PersistDbConfig::load().unwrap();
            let rx = settings.theme().on_change();
            let other = SqliteBackend::open(db.path()).unwrap();

            other
                .set("theme", &StoredValue::from_raw("not-json".to_string()))
                .unwrap();

            assert!(rx.recv_timeout(Duration::from_millis(1500)).is_err());
            assert_eq!(settings.theme().get(), "system");
        },
    );
}

#[test]
fn cross_process_false_disables_external_change_detection() {
    let db = tempfile::NamedTempFile::new().unwrap();

    temp_env::with_var(
        "DERIVE_PERSIST_NO_CROSS_PROCESS_TEST_CONFIG",
        Some(db.path().to_str().unwrap()),
        || {
            let settings = PersistNoCrossProcessConfig::load().unwrap();
            let rx = settings.on_change();
            let other = SqliteBackend::open(db.path()).unwrap();

            other
                .set(
                    "theme",
                    &StoredValue::encode(&"external".to_string()).unwrap(),
                )
                .unwrap();

            assert!(rx.recv_timeout(Duration::from_millis(300)).is_err());
            assert_eq!(settings.theme().get(), "system");
        },
    );
}

#[test]
fn persist_field_loads_stored_value_from_custom_key() {
    let db = tempfile::NamedTempFile::new().unwrap();
    let backend = SqliteBackend::open(db.path()).unwrap();
    backend
        .set(
            "log.level",
            &StoredValue::encode(&"debug".to_string()).unwrap(),
        )
        .unwrap();

    temp_env::with_var(
        "DERIVE_PERSIST_KEY_TEST_CONFIG",
        Some(db.path().to_str().unwrap()),
        || {
            let settings = PersistKeyConfig::load().unwrap();

            assert_eq!(settings.log_level().get(), "debug");
        },
    );
}

#[test]
fn persist_field_setter_emits_custom_key() {
    let db = tempfile::NamedTempFile::new().unwrap();

    temp_env::with_var(
        "DERIVE_PERSIST_KEY_TEST_CONFIG",
        Some(db.path().to_str().unwrap()),
        || {
            let settings = PersistKeyConfig::load().unwrap();
            let rx = settings.on_change();

            settings.log_level().set("debug".to_string()).unwrap();

            let event = rx.recv().unwrap();
            match event {
                ChangeEvent::Set { key, .. } => assert_eq!(key, "log.level"),
                event => panic!("unexpected event: {event:?}"),
            }
        },
    );
}

#[test]
fn external_persist_field_write_uses_custom_key() {
    let db = tempfile::NamedTempFile::new().unwrap();

    temp_env::with_var(
        "DERIVE_PERSIST_KEY_TEST_CONFIG",
        Some(db.path().to_str().unwrap()),
        || {
            let settings = PersistKeyConfig::load().unwrap();
            let rx = settings.on_change();
            let other = SqliteBackend::open(db.path()).unwrap();

            other
                .set(
                    "log.level",
                    &StoredValue::encode(&"debug".to_string()).unwrap(),
                )
                .unwrap();

            let event = rx.recv_timeout(Duration::from_secs(3)).unwrap();
            assert!(matches!(
                event,
                ChangeEvent::Set {
                    key,
                    source: ChangeSource::External,
                    ..
                } if key == "log.level"
            ));
            assert_eq!(settings.log_level().get(), "debug");
        },
    );
}

#[test]
fn persist_field_load_uses_deserialize_fallback_for_legacy_value() {
    let db = tempfile::NamedTempFile::new().unwrap();
    let backend = SqliteBackend::open(db.path()).unwrap();
    backend
        .set(
            "port",
            &StoredValue::encode(&"legacy-port".to_string()).unwrap(),
        )
        .unwrap();

    temp_env::with_var(
        "DERIVE_PERSIST_DESERIALIZE_FALLBACK_TEST_CONFIG",
        Some(db.path().to_str().unwrap()),
        || {
            let settings = PersistDeserializeFallbackConfig::load().unwrap();

            assert_eq!(settings.port().get(), 9000);
        },
    );
}

#[test]
fn persist_field_load_returns_error_when_deserialize_fallback_fails() {
    let db = tempfile::NamedTempFile::new().unwrap();
    let backend = SqliteBackend::open(db.path()).unwrap();
    backend
        .set(
            "port",
            &StoredValue::encode(&"unknown".to_string()).unwrap(),
        )
        .unwrap();

    temp_env::with_var(
        "DERIVE_PERSIST_DESERIALIZE_FALLBACK_TEST_CONFIG",
        Some(db.path().to_str().unwrap()),
        || {
            let error = match PersistDeserializeFallbackConfig::load() {
                Ok(_) => panic!("expected deserialize fallback error"),
                Err(error) => error,
            };

            match error {
                SettingsError::PersistFallbackParse { key, error } => {
                    assert_eq!(key, "port");
                    assert!(error.contains("unknown legacy port"));
                }
                error => panic!("unexpected error: {error:?}"),
            }
        },
    );
}

#[test]
fn external_persist_field_write_uses_deserialize_fallback() {
    let db = tempfile::NamedTempFile::new().unwrap();

    temp_env::with_var(
        "DERIVE_PERSIST_DESERIALIZE_FALLBACK_TEST_CONFIG",
        Some(db.path().to_str().unwrap()),
        || {
            let settings = PersistDeserializeFallbackConfig::load().unwrap();
            let all_rx = settings.on_change();
            let port_rx = settings.port().on_change();
            let other = SqliteBackend::open(db.path()).unwrap();

            other
                .set(
                    "port",
                    &StoredValue::encode(&"legacy-port".to_string()).unwrap(),
                )
                .unwrap();

            let event = all_rx.recv_timeout(Duration::from_secs(3)).unwrap();
            match event {
                ChangeEvent::Set { key, new_value, .. } => {
                    assert_eq!(key, "port");
                    assert_eq!(new_value, StoredValue::encode(&9000).unwrap());
                }
                event => panic!("unexpected event: {event:?}"),
            }
            assert_eq!(port_rx.recv_timeout(Duration::from_secs(1)).unwrap(), 9000);
            assert_eq!(settings.port().get(), 9000);
        },
    );
}

#[test]
fn failed_external_deserialize_fallback_preserves_current_value() {
    let db = tempfile::NamedTempFile::new().unwrap();

    temp_env::with_var(
        "DERIVE_PERSIST_DESERIALIZE_FALLBACK_TEST_CONFIG",
        Some(db.path().to_str().unwrap()),
        || {
            let settings = PersistDeserializeFallbackConfig::load().unwrap();
            let all_rx = settings.on_change();
            let port_rx = settings.port().on_change();
            let other = SqliteBackend::open(db.path()).unwrap();

            other
                .set(
                    "port",
                    &StoredValue::encode(&"unknown".to_string()).unwrap(),
                )
                .unwrap();

            let event = all_rx.recv_timeout(Duration::from_secs(3)).unwrap();
            match event {
                ChangeEvent::DeserializeFailure {
                    key, raw, error, ..
                } => {
                    assert_eq!(key, "port");
                    assert_eq!(raw, "\"unknown\"");
                    assert!(error.contains("unknown legacy port"));
                }
                event => panic!("unexpected event: {event:?}"),
            }
            assert!(port_rx.recv_timeout(Duration::from_millis(300)).is_err());
            assert_eq!(settings.port().get(), 80);
        },
    );
}
