use super::*;
use crate::config::RateLimitBps;

#[tokio::test]
async fn save_sections_preserves_other_tables_and_comments() {
    let dir = std::env::temp_dir().join(format!("cfgtest-{}", rand::random::<u64>()));
    std::fs::create_dir_all(&dir).unwrap();
    let path = dir.join("config.toml");
    std::fs::write(
        &path,
        "# top comment\n[censorship]\ntls_domain = \"old.example\"\n\n[server]\nport = 443\n",
    )
    .unwrap();

    let mut cfg = ProxyConfig::default();
    cfg.censorship.tls_domain = "new.example".to_string();
    cfg.server.port = 443;

    let rev = save_sections_to_disk(&path, &cfg, &["censorship"])
        .await
        .unwrap();

    let written = std::fs::read_to_string(&path).unwrap();
    assert!(written.contains("tls_domain = \"new.example\""));
    // Untouched comments and tables remain byte content.
    assert!(written.contains("# top comment"));
    assert!(written.contains("[server]\nport = 443"));
    assert_eq!(rev, compute_revision(&written));
    std::fs::remove_dir_all(&dir).ok();
}

#[test]
fn find_bounds_matches_array_of_tables() {
    let src = "[server]\nport = 1\n\n[[upstreams]]\nkind = \"a\"\n\n[[upstreams]]\nkind = \"b\"\n";
    let bounds = find_toml_table_bounds(src, "upstreams");
    assert!(bounds.is_some(), "should locate [[upstreams]] block start");
    let (start, end) = bounds.unwrap();
    let slice = &src[start..end];
    assert!(slice.starts_with("[[upstreams]]"));
    // The bound spans through the last upstream block.
    assert!(slice.contains("kind = \"b\""));
}

#[test]
fn find_bounds_matches_header_with_inline_comment() {
    let src = "[censorship] # notes\ntls_domain = \"a\"\n\n[server]\nport = 1\n";
    let bounds = find_toml_table_bounds(src, "censorship");
    assert!(bounds.is_some(), "commented header must still match");
    let (start, end) = bounds.unwrap();
    let slice = &src[start..end];
    assert!(slice.starts_with("[censorship] # notes"));
    assert!(slice.contains("tls_domain"));
    // The bound terminates at the next header.
    assert!(!slice.contains("[server]"));
}

#[tokio::test]
async fn save_general_section_keeps_subtables_dotted_without_duplicates() {
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("config.toml");
    tokio::fs::write(
        &path,
        "[general]\nprefer_ipv6 = false\n\n[general.modes]\ntls = true\n\n\
         [general.links]\npublic_host = \"old.example\"\n\n[server]\nport = 443\n",
    )
    .await
    .unwrap();

    let mut cfg = ProxyConfig::default();
    cfg.general.prefer_ipv6 = true;

    save_sections_to_disk(&path, &cfg, &["general"])
        .await
        .unwrap();

    let written = tokio::fs::read_to_string(&path).await.unwrap();

    // No bare top-level [modes] / [links] headers leaked.
    for line in written.lines() {
        let header = line.trim();
        assert_ne!(header, "[modes]", "leaked top-level [modes]:\n{written}");
        assert_ne!(header, "[links]", "leaked top-level [links]:\n{written}");
    }

    // Sub-tables kept their dotted prefix exactly once each.
    assert_eq!(
        written.matches("[general.modes]").count(),
        1,
        "[general.modes] must appear exactly once:\n{written}"
    );
    assert_eq!(
        written.matches("[general.links]").count(),
        1,
        "[general.links] must appear exactly once:\n{written}"
    );

    // Result parses (duplicate tables would error here).
    toml::from_str::<toml::Value>(&written)
        .unwrap_or_else(|e| panic!("written config must parse: {e}\n{written}"));

    // The unrelated table remains untouched.
    assert!(written.contains("[server]\nport = 443"));
}

#[tokio::test]
async fn save_general_section_is_idempotent_across_repeated_saves() {
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("config.toml");
    tokio::fs::write(
        &path,
        "[general]\nprefer_ipv6 = false\n\n[general.modes]\ntls = true\n\n\
         [general.links]\npublic_host = \"old.example\"\n",
    )
    .await
    .unwrap();

    let mut cfg = ProxyConfig::default();
    cfg.general.prefer_ipv6 = true;

    save_sections_to_disk(&path, &cfg, &["general"])
        .await
        .unwrap();
    save_sections_to_disk(&path, &cfg, &["general"])
        .await
        .unwrap();

    let written = tokio::fs::read_to_string(&path).await.unwrap();
    assert_eq!(written.matches("[general.modes]").count(), 1, "{written}");
    assert_eq!(written.matches("[general.links]").count(), 1, "{written}");
    assert_eq!(written.matches("[general]").count(), 1, "{written}");
    toml::from_str::<toml::Value>(&written)
        .unwrap_or_else(|e| panic!("written config must parse: {e}\n{written}"));
}

#[test]
fn find_bounds_spans_dotted_subtables() {
    let src = "[general]\nprefer_ipv6 = false\n\n[general.modes]\ntls = true\n\n\
               [general.links]\npublic_host = \"a\"\n\n[server]\nport = 1\n";
    let bounds = find_toml_table_bounds(src, "general");
    assert!(bounds.is_some(), "should locate [general] block");
    let (start, end) = bounds.unwrap();
    let slice = &src[start..end];
    assert!(slice.starts_with("[general]"));
    // Nested sub-tables belong to the parent table bound.
    assert!(slice.contains("[general.modes]"));
    assert!(slice.contains("[general.links]"));
    // The bound terminates before an unrelated header.
    assert!(!slice.contains("[server]"));
}

#[test]
fn find_bounds_does_not_overrun_sibling_prefix() {
    // access.users must not swallow access.user_enabled (dot guards the prefix).
    let src = "[access.users]\nalice = \"x\"\n\n[access.user_enabled]\nalice = true\n";
    let bounds = find_toml_table_bounds(src, "access.users").unwrap();
    let slice = &src[bounds.0..bounds.1];
    assert!(slice.starts_with("[access.users]"));
    assert!(!slice.contains("[access.user_enabled]"));
}

#[test]
fn nested_include_detection_does_not_reject_similar_access_keys() {
    assert!(has_include_inside_table(
        "[access.users]\ninclude = \"users.toml\"\n"
    ));
    assert!(!has_include_inside_table(
        "[access.users]\ninclude_user = \"00000000000000000000000000000000\"\n"
    ));
}

#[tokio::test]
async fn save_general_handles_non_contiguous_subtables() {
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("config.toml");
    // Hand-edited layout: [general.modes] sits AFTER an unrelated [server].
    tokio::fs::write(
        &path,
        "[general]\nprefer_ipv6 = false\n\n[server]\nport = 443\n\n\
         [general.modes]\ntls = true\n",
    )
    .await
    .unwrap();

    let mut cfg = ProxyConfig::default();
    cfg.general.prefer_ipv6 = true;

    save_sections_to_disk(&path, &cfg, &["general"])
        .await
        .unwrap();

    let written = tokio::fs::read_to_string(&path).await.unwrap();
    assert_eq!(
        written.matches("[general.modes]").count(),
        1,
        "non-contiguous [general.modes] must not duplicate:\n{written}"
    );
    toml::from_str::<toml::Value>(&written)
        .unwrap_or_else(|e| panic!("written config must parse: {e}\n{written}"));
    // The unrelated section remains present.
    assert!(written.contains("[server]"));
}

#[tokio::test]
async fn manifest_revision_changes_when_an_included_source_changes() {
    let dir = tempfile::tempdir().unwrap();
    let root = dir.path().join("config.toml");
    let included = dir.path().join("included.toml");
    tokio::fs::write(&root, "include = \"included.toml\"\n")
        .await
        .unwrap();
    tokio::fs::write(&included, "[censorship]\ntls_domain = \"one.example\"\n")
        .await
        .unwrap();
    let first = current_revision(&root).await.unwrap();

    tokio::fs::write(&included, "[censorship]\ntls_domain = \"two.example\"\n")
        .await
        .unwrap();
    let second = current_revision(&root).await.unwrap();

    assert_ne!(first, second);
}

#[tokio::test]
async fn manifest_revision_does_not_require_typed_config_validation() {
    let dir = tempfile::tempdir().unwrap();
    let root = dir.path().join("config.toml");
    tokio::fs::write(&root, "[server]\nport = \"invalid\"\n")
        .await
        .unwrap();

    let revision = current_revision(&root).await.unwrap();

    assert_eq!(revision.len(), 64);
    assert!(load_config_snapshot(&root, false).await.is_err());
}

#[tokio::test]
async fn access_mutation_writes_only_the_single_included_owner() {
    let dir = tempfile::tempdir().unwrap();
    let root = dir.path().join("config.toml");
    let included = dir.path().join("users.toml");
    let root_body = "include = \"users.toml\"\n[censorship]\ntls_domain = \"example.com\"\n";
    let included_body = "[access.users]\nalice = \"00000000000000000000000000000000\"\n";
    tokio::fs::write(&root, root_body).await.unwrap();
    tokio::fs::write(&included, included_body).await.unwrap();
    let mut cfg = load_config_from_disk(&root).await.unwrap();
    cfg.access.users.insert(
        "bob".to_string(),
        "11111111111111111111111111111111".to_string(),
    );

    let revision = save_access_sections_to_disk(&root, &cfg, &[AccessSection::Users])
        .await
        .unwrap();

    assert_eq!(tokio::fs::read_to_string(&root).await.unwrap(), root_body);
    let written = tokio::fs::read_to_string(&included).await.unwrap();
    assert!(written.contains("bob = \"11111111111111111111111111111111\""));
    assert_eq!(revision, current_revision(&root).await.unwrap());
}

#[tokio::test]
async fn access_mutation_rejects_sections_with_different_source_owners() {
    let dir = tempfile::tempdir().unwrap();
    let root = dir.path().join("config.toml");
    let included = dir.path().join("enabled.toml");
    let root_body = concat!(
        "include = \"enabled.toml\"\n",
        "[access.users]\nalice = \"00000000000000000000000000000000\"\n"
    );
    let included_body = "[access.user_enabled]\nalice = false\n";
    tokio::fs::write(&root, root_body).await.unwrap();
    tokio::fs::write(&included, included_body).await.unwrap();
    let cfg = load_config_from_disk(&root).await.unwrap();

    let error = save_access_sections_to_disk(
        &root,
        &cfg,
        &[AccessSection::Users, AccessSection::UserEnabled],
    )
    .await
    .unwrap_err();

    assert_eq!(error.code, "config_patch_not_atomic");
    assert_eq!(tokio::fs::read_to_string(&root).await.unwrap(), root_body);
    assert_eq!(
        tokio::fs::read_to_string(&included).await.unwrap(),
        included_body
    );
}

#[test]
fn render_user_rate_limits_section() {
    let mut cfg = ProxyConfig::default();
    cfg.access.user_rate_limits.insert(
        "alice".to_string(),
        RateLimitBps {
            up_bps: 1024,
            down_bps: 2048,
        },
    );

    let rendered =
        render_access_section(&cfg, AccessSection::UserRateLimits).expect("section must render");

    assert!(rendered.starts_with("[access.user_rate_limits]\n"));
    assert!(rendered.contains("alice = { up_bps = 1024, down_bps = 2048 }"));
}
