use std::fs;

use serial_test::serial;

use super::*;

struct XdgGuard {
    original: Option<String>
}

impl XdgGuard {
    fn set(dir: &std::path::Path) -> Self {
        let original = std::env::var("XDG_CONFIG_HOME").ok();
        unsafe {
            std::env::set_var("XDG_CONFIG_HOME", dir);
        }
        Self {
            original
        }
    }
}

impl Drop for XdgGuard {
    fn drop(&mut self) {
        unsafe {
            match &self.original {
                Some(v) => std::env::set_var("XDG_CONFIG_HOME", v),
                None => std::env::remove_var("XDG_CONFIG_HOME")
            }
        }
    }
}

#[test]
fn default_config_has_no_token() {
    let cfg = AppConfig::default();
    assert!(cfg.token.is_none());
}

#[test]
fn path_returns_valid_toml_path() {
    let path = AppConfig::path().expect("config path should resolve");
    assert!(path.to_string_lossy().ends_with("config.toml"));
}

#[test]
fn path_contains_twc_rs_dir() {
    let path = AppConfig::path().expect("config path should resolve");
    let path_str = path.to_string_lossy();
    assert!(
        path_str.contains("twc-rs"),
        "Path should contain twc-rs dir: {path_str}"
    );
}

#[test]
#[serial]
fn load_returns_default_when_no_file() {
    let dir = tempfile::tempdir().unwrap();
    let _guard = XdgGuard::set(dir.path());
    let cfg = AppConfig::load().expect("load should succeed");
    assert!(cfg.token.is_none());
}

#[test]
#[serial]
fn load_returns_default_when_dir_exists_but_no_file() {
    let dir = tempfile::tempdir().unwrap();
    let config_dir = dir.path().join("twc-rs");
    fs::create_dir_all(&config_dir).unwrap();
    let _guard = XdgGuard::set(dir.path());
    let cfg = AppConfig::load().expect("load should succeed");
    assert!(cfg.token.is_none());
}

#[test]
#[serial]
fn load_parses_valid_toml_with_token() {
    let dir = tempfile::tempdir().unwrap();
    let config_dir = dir.path().join("twc-rs");
    fs::create_dir_all(&config_dir).unwrap();
    fs::write(
        config_dir.join("config.toml"),
        "token = \"my-secret-token\"\n"
    )
    .unwrap();
    let _guard = XdgGuard::set(dir.path());
    let cfg = AppConfig::load().expect("load should succeed");
    assert_eq!(cfg.token.as_deref(), Some("my-secret-token"));
}

#[test]
#[serial]
fn load_returns_config_parse_error_for_invalid_toml() {
    let dir = tempfile::tempdir().unwrap();
    let config_dir = dir.path().join("twc-rs");
    fs::create_dir_all(&config_dir).unwrap();
    fs::write(config_dir.join("config.toml"), "this is not [[valid toml").unwrap();
    let _guard = XdgGuard::set(dir.path());
    let err = AppConfig::load().unwrap_err();
    assert!(
        err.to_string().contains("failed to parse config"),
        "Expected parse error, got: {err}"
    );
}

#[test]
#[serial]
fn save_and_load_roundtrip() {
    let dir = tempfile::tempdir().unwrap();
    let _guard = XdgGuard::set(dir.path());
    let cfg = AppConfig {
        token: Some("roundtrip-token".to_string()),
        ..AppConfig::default()
    };
    cfg.save().expect("save should succeed");
    let loaded = AppConfig::load().expect("load should succeed");
    assert_eq!(loaded.token.as_deref(), Some("roundtrip-token"));
    let path = AppConfig::path().unwrap();
    assert!(path.exists());
}

#[test]
#[serial]
fn save_creates_parent_directories() {
    let dir = tempfile::tempdir().unwrap();
    let _guard = XdgGuard::set(dir.path());
    let cfg = AppConfig {
        token: Some("test".to_string()),
        ..AppConfig::default()
    };
    cfg.save().expect("save should succeed");
    let path = AppConfig::path().unwrap();
    assert!(path.parent().unwrap().exists());
}

#[test]
#[serial]
fn load_without_token_field() {
    let dir = tempfile::tempdir().unwrap();
    let config_dir = dir.path().join("twc-rs");
    fs::create_dir_all(&config_dir).unwrap();
    fs::write(config_dir.join("config.toml"), "").unwrap();
    let _guard = XdgGuard::set(dir.path());
    let cfg = AppConfig::load().expect("load should succeed");
    assert!(cfg.token.is_none());
}

#[test]
#[serial]
fn save_overwrites_existing_config() {
    let dir = tempfile::tempdir().unwrap();
    let _guard = XdgGuard::set(dir.path());
    let cfg1 = AppConfig {
        token: Some("first".to_string()),
        ..AppConfig::default()
    };
    cfg1.save().unwrap();
    let cfg2 = AppConfig {
        token: Some("second".to_string()),
        ..AppConfig::default()
    };
    cfg2.save().unwrap();
    let loaded = AppConfig::load().unwrap();
    assert_eq!(loaded.token.as_deref(), Some("second"));
}

#[cfg(unix)]
#[test]
#[serial]
fn save_restricts_file_and_dir_permissions() {
    use std::os::unix::fs::PermissionsExt;

    let dir = tempfile::tempdir().unwrap();
    let _guard = XdgGuard::set(dir.path());
    let cfg = AppConfig {
        token: Some("secret".to_string()),
        ..AppConfig::default()
    };
    cfg.save().expect("save should succeed");

    let path = AppConfig::path().unwrap();
    let file_mode = fs::metadata(&path).unwrap().permissions().mode() & 0o777;
    assert_eq!(file_mode, 0o600, "config file must be owner-only");

    let dir_mode = fs::metadata(path.parent().unwrap())
        .unwrap()
        .permissions()
        .mode()
        & 0o777;
    assert_eq!(dir_mode, 0o700, "config dir must be owner-only");
}

#[cfg(unix)]
#[test]
#[serial]
fn save_tightens_permissions_on_preexisting_file() {
    use std::os::unix::fs::PermissionsExt;

    let dir = tempfile::tempdir().unwrap();
    let _guard = XdgGuard::set(dir.path());
    let path = AppConfig::path().unwrap();
    fs::create_dir_all(path.parent().unwrap()).unwrap();
    fs::write(&path, "token = \"stale\"\n").unwrap();
    fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).unwrap();

    let cfg = AppConfig {
        token: Some("fresh".to_string()),
        ..AppConfig::default()
    };
    cfg.save().expect("save should succeed");

    let file_mode = fs::metadata(&path).unwrap().permissions().mode() & 0o777;
    assert_eq!(
        file_mode, 0o600,
        "existing config file must be re-restricted"
    );
}

#[test]
fn serialization_roundtrip_via_string() {
    let cfg = AppConfig {
        token: Some("my-secret-token".to_string()),
        ..AppConfig::default()
    };
    let toml_str = toml::to_string(&cfg).unwrap();
    let loaded: AppConfig = toml::from_str(&toml_str).unwrap();
    assert_eq!(loaded.token.as_deref(), Some("my-secret-token"));
}

#[test]
fn serialization_roundtrip_empty() {
    let cfg = AppConfig::default();
    let toml_str = toml::to_string(&cfg).unwrap();
    let loaded: AppConfig = toml::from_str(&toml_str).unwrap();
    assert!(loaded.token.is_none());
}

#[test]
fn deserialize_without_token_field() {
    let cfg: AppConfig = toml::from_str("").unwrap();
    assert!(cfg.token.is_none());
}

#[test]
fn deserialize_with_token() {
    let cfg: AppConfig = toml::from_str("token = \"abc123\"\n").unwrap();
    assert_eq!(cfg.token.as_deref(), Some("abc123"));
}

#[test]
fn deserialize_invalid_toml() {
    let result = toml::from_str::<AppConfig>("not [[valid");
    assert!(result.is_err());
}

#[test]
fn serialize_produces_valid_toml() {
    let cfg = AppConfig {
        token: Some("tok".to_string()),
        ..AppConfig::default()
    };
    let toml_str = toml::to_string(&cfg).unwrap();
    assert!(toml_str.contains("token"));
    assert!(toml_str.contains("tok"));
}

#[test]
fn serialize_skips_none_token() {
    let cfg = AppConfig::default();
    let toml_str = toml::to_string(&cfg).unwrap();
    assert!(!toml_str.contains("token"));
}

#[test]
fn save_and_load_roundtrip_filesystem() {
    let dir = tempfile::tempdir().unwrap();
    let file_path = dir.path().join("config.toml");
    let cfg = AppConfig {
        token: Some("roundtrip-token".to_string()),
        ..AppConfig::default()
    };
    let content = toml::to_string(&cfg).unwrap();
    fs::write(&file_path, content).unwrap();
    let read_back = fs::read_to_string(&file_path).unwrap();
    let loaded: AppConfig = toml::from_str(&read_back).unwrap();
    assert_eq!(loaded.token.as_deref(), Some("roundtrip-token"));
}

#[test]
fn save_creates_parent_directories_filesystem() {
    let dir = tempfile::tempdir().unwrap();
    let config_dir = dir.path().join("twc-rs");
    fs::create_dir_all(&config_dir).unwrap();
    let file_path = config_dir.join("config.toml");
    let cfg = AppConfig {
        token: Some("test".to_string()),
        ..AppConfig::default()
    };
    let content = toml::to_string(&cfg).unwrap();
    fs::write(&file_path, content).unwrap();
    assert!(file_path.exists());
    assert!(file_path.parent().unwrap().exists());
}

#[test]
fn load_from_filesystem_invalid_toml() {
    let dir = tempfile::tempdir().unwrap();
    let file_path = dir.path().join("config.toml");
    fs::write(&file_path, "this is not [[valid toml").unwrap();
    let content = fs::read_to_string(&file_path).unwrap();
    let result = toml::from_str::<AppConfig>(&content);
    assert!(result.is_err());
}

#[test]
fn deserialize_with_extra_fields_ignored() {
    let cfg: AppConfig = toml::from_str("token = \"abc\"\nother_field = true\n").unwrap();
    assert_eq!(cfg.token.as_deref(), Some("abc"));
}

#[test]
fn clone_config() {
    let cfg = AppConfig {
        token: Some("cloned".to_string()),
        ..AppConfig::default()
    };
    let cloned = cfg;
    assert_eq!(cloned.token.as_deref(), Some("cloned"));
}

#[test]
fn debug_format() {
    let cfg = AppConfig {
        token: Some("debug".to_string()),
        ..AppConfig::default()
    };
    let debug_str = format!("{cfg:?}");
    assert!(debug_str.contains("AppConfig"));
    assert!(debug_str.contains("debug"));
}

#[test]
fn language_locale_en() {
    assert_eq!(Language::En.locale(), "en");
}

#[test]
fn language_locale_ru() {
    assert_eq!(Language::Ru.locale(), "ru");
}

#[test]
fn language_default_is_en() {
    assert_eq!(Language::default(), Language::En);
}

#[test]
fn token_for_none_returns_default_token() {
    let cfg = AppConfig {
        token: Some("default-tok".to_string()),
        ..AppConfig::default()
    };
    let got = cfg.token_for(None).unwrap();
    assert_eq!(got.as_deref(), Some("default-tok"));
}

#[test]
fn token_for_none_returns_none_when_unset() {
    let cfg = AppConfig::default();
    assert!(cfg.token_for(None).unwrap().is_none());
}

#[test]
fn token_for_existing_profile() {
    let mut cfg = AppConfig::default();
    cfg.profiles
        .insert("work".to_string(), "work-tok".to_string());
    let got = cfg.token_for(Some("work")).unwrap();
    assert_eq!(got.as_deref(), Some("work-tok"));
}

#[test]
fn token_for_missing_profile_errors() {
    let cfg = AppConfig::default();
    let err = cfg.token_for(Some("ghost")).unwrap_err();
    assert!(err.to_string().contains("profile 'ghost' not found"));
}

#[test]
fn path_ends_with_expected_segments() {
    let path = AppConfig::path().unwrap();
    assert!(path.ends_with("twc-rs/config.toml"));
}

#[test]
fn save_to_fails_when_path_is_a_directory() {
    let dir = tempfile::tempdir().unwrap();
    let target = dir.path().join("as-dir");
    fs::create_dir(&target).unwrap();
    let cfg = AppConfig {
        token: Some("x".to_string()),
        ..AppConfig::default()
    };
    let err = cfg.save_to(&target).unwrap_err();
    assert!(err.to_string().contains("failed to write"));
}

#[cfg(unix)]
#[test]
fn restrict_dir_permissions_errors_on_missing_dir() {
    let dir = tempfile::tempdir().unwrap();
    let missing = dir.path().join("does-not-exist");
    let err = super::restrict_dir_permissions(&missing).unwrap_err();
    assert!(err.to_string().contains("failed to set permissions"));
}

#[test]
fn save_to_fails_when_parent_is_a_file() {
    let dir = tempfile::tempdir().unwrap();
    let file_as_parent = dir.path().join("afile");
    fs::write(&file_as_parent, "x").unwrap();
    let target = file_as_parent.join("nested").join("config.toml");
    let cfg = AppConfig {
        token: Some("x".to_string()),
        ..AppConfig::default()
    };
    let err = cfg.save_to(&target).unwrap_err();
    assert!(err.to_string().contains("failed to create dir"));
}
