use super::*;
use codex_config::NetworkDomainPermissionToml;
use codex_config::NetworkDomainPermissionsToml;
use codex_network_proxy::NetworkDomainPermission;
use codex_protocol::models::ManagedFileSystemPermissions;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::SandboxPolicy;
use pretty_assertions::assert_eq;

fn permission_profile_for_sandbox_policy(sandbox_policy: &SandboxPolicy) -> PermissionProfile {
    PermissionProfile::from_legacy_sandbox_policy(sandbox_policy)
}

fn domain_permissions(
    entries: impl IntoIterator<Item = (&'static str, NetworkDomainPermissionToml)>,
) -> NetworkDomainPermissionsToml {
    NetworkDomainPermissionsToml {
        entries: entries
            .into_iter()
            .map(|(pattern, permission)| (pattern.to_string(), permission))
            .collect(),
    }
}

#[test]
fn build_state_with_audit_metadata_threads_metadata_to_state() {
    let spec = NetworkProxySpec {
        base_config: NetworkProxyConfig::default(),
        requirements: None,
        config: NetworkProxyConfig::default(),
        constraints: NetworkProxyConstraints::default(),
        hard_deny_allowlist_misses: false,
    };
    let metadata = NetworkProxyAuditMetadata {
        conversation_id: Some("conversation-1".to_string()),
        app_version: Some("1.2.3".to_string()),
        user_account_id: Some("acct-1".to_string()),
        ..NetworkProxyAuditMetadata::default()
    };

    let state = spec
        .build_state_with_audit_metadata(metadata.clone())
        .expect("state should build");
    assert_eq!(state.audit_metadata(), &metadata);
}

#[test]
fn requirements_allowed_domains_are_a_baseline_for_user_allowlist() {
    let mut config = NetworkProxyConfig::default();
    config
        .network
        .set_allowed_domains(vec!["api.example.com".to_string()]);
    let requirements = NetworkConstraints {
        domains: Some(domain_permissions([(
            "*.example.com",
            NetworkDomainPermissionToml::Allow,
        )])),
        ..Default::default()
    };

    let spec = NetworkProxySpec::from_config_and_constraints(
        config,
        Some(requirements),
        &permission_profile_for_sandbox_policy(&SandboxPolicy::new_read_only_policy()),
    )
    .expect("config should stay within the managed allowlist");

    assert_eq!(
        spec.config.network.allowed_domains(),
        Some(vec![
            "*.example.com".to_string(),
            "api.example.com".to_string()
        ])
    );
    assert_eq!(
        spec.constraints.allowed_domains,
        Some(vec!["*.example.com".to_string()])
    );
    assert_eq!(spec.constraints.allowlist_expansion_enabled, Some(true));
}

#[test]
fn requirements_allowed_domains_do_not_override_user_denies_for_same_pattern() {
    let mut config = NetworkProxyConfig::default();
    config
        .network
        .set_denied_domains(vec!["api.example.com".to_string()]);
    let requirements = NetworkConstraints {
        domains: Some(domain_permissions([(
            "api.example.com",
            NetworkDomainPermissionToml::Allow,
        )])),
        ..Default::default()
    };

    let spec = NetworkProxySpec::from_config_and_constraints(
        config,
        Some(requirements),
        &permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()),
    )
    .expect("managed allowlist should not erase a user deny");

    assert_eq!(spec.config.network.allowed_domains(), None);
    assert_eq!(
        spec.config.network.denied_domains(),
        Some(vec!["api.example.com".to_string()])
    );
    assert_eq!(
        spec.constraints.allowed_domains,
        Some(vec!["api.example.com".to_string()])
    );
}

#[test]
fn requirements_allowlist_expansion_keeps_user_entries_mutable() {
    let mut config = NetworkProxyConfig::default();
    config
        .network
        .set_allowed_domains(vec!["api.example.com".to_string()]);
    let requirements = NetworkConstraints {
        domains: Some(domain_permissions([(
            "*.example.com",
            NetworkDomainPermissionToml::Allow,
        )])),
        ..Default::default()
    };

    let spec = NetworkProxySpec::from_config_and_constraints(
        config,
        Some(requirements),
        &permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()),
    )
    .expect("managed baseline should still allow user edits");

    let mut candidate = spec.config.clone();
    candidate.network.upsert_domain_permission(
        "api.example.com".to_string(),
        NetworkDomainPermission::Deny,
        normalize_host,
    );

    assert_eq!(
        candidate.network.allowed_domains(),
        Some(vec!["*.example.com".to_string()])
    );
    assert_eq!(
        candidate.network.denied_domains(),
        Some(vec!["api.example.com".to_string()])
    );
    validate_policy_against_constraints(&candidate, &spec.constraints)
        .expect("user allowlist entries should not become managed constraints");
}

#[test]
fn managed_unrestricted_profile_allows_domain_expansion() {
    let mut config = NetworkProxyConfig::default();
    config
        .network
        .set_allowed_domains(vec!["api.example.com".to_string()]);
    let requirements = NetworkConstraints {
        domains: Some(domain_permissions([(
            "*.example.com",
            NetworkDomainPermissionToml::Allow,
        )])),
        ..Default::default()
    };
    let permission_profile = PermissionProfile::Managed {
        file_system: ManagedFileSystemPermissions::Unrestricted,
        network: NetworkSandboxPolicy::Restricted,
    };

    let spec = NetworkProxySpec::from_config_and_constraints(
        config,
        Some(requirements),
        &permission_profile,
    )
    .expect("managed unrestricted filesystem should still use managed network constraints");

    assert_eq!(
        spec.config.network.allowed_domains(),
        Some(vec![
            "*.example.com".to_string(),
            "api.example.com".to_string()
        ])
    );
    assert_eq!(spec.constraints.allowlist_expansion_enabled, Some(true));
}

#[test]
fn danger_full_access_keeps_managed_allowlist_and_denylist_fixed() {
    let mut config = NetworkProxyConfig::default();
    config
        .network
        .set_allowed_domains(vec!["evil.com".to_string()]);
    config
        .network
        .set_denied_domains(vec!["more-blocked.example.com".to_string()]);
    let requirements = NetworkConstraints {
        domains: Some(domain_permissions([
            ("*.example.com", NetworkDomainPermissionToml::Allow),
            ("blocked.example.com", NetworkDomainPermissionToml::Deny),
        ])),
        ..Default::default()
    };

    let spec = NetworkProxySpec::from_config_and_constraints(
        config,
        Some(requirements),
        &permission_profile_for_sandbox_policy(&SandboxPolicy::DangerFullAccess),
    )
    .expect("yolo mode should pin the effective policy to the managed baseline");

    assert_eq!(
        spec.config.network.allowed_domains(),
        Some(vec!["*.example.com".to_string()])
    );
    assert_eq!(
        spec.config.network.denied_domains(),
        Some(vec!["blocked.example.com".to_string()])
    );
    assert_eq!(spec.constraints.allowlist_expansion_enabled, Some(false));
    assert_eq!(spec.constraints.denylist_expansion_enabled, Some(false));
}

#[test]
fn managed_allowed_domains_only_disables_default_mode_allowlist_expansion() {
    let mut config = NetworkProxyConfig::default();
    config
        .network
        .set_allowed_domains(vec!["api.example.com".to_string()]);
    let requirements = NetworkConstraints {
        domains: Some(domain_permissions([(
            "*.example.com",
            NetworkDomainPermissionToml::Allow,
        )])),
        managed_allowed_domains_only: Some(true),
        ..Default::default()
    };

    let spec = NetworkProxySpec::from_config_and_constraints(
        config,
        Some(requirements),
        &permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()),
    )
    .expect("managed baseline should still load");

    assert_eq!(
        spec.config.network.allowed_domains(),
        Some(vec!["*.example.com".to_string()])
    );
    assert_eq!(spec.constraints.allowlist_expansion_enabled, Some(false));
}

#[test]
fn managed_allowed_domains_only_ignores_user_allowlist_and_hard_denies_misses() {
    let mut config = NetworkProxyConfig::default();
    config
        .network
        .set_allowed_domains(vec!["api.example.com".to_string()]);
    let requirements = NetworkConstraints {
        domains: Some(domain_permissions([(
            "managed.example.com",
            NetworkDomainPermissionToml::Allow,
        )])),
        managed_allowed_domains_only: Some(true),
        ..Default::default()
    };

    let spec = NetworkProxySpec::from_config_and_constraints(
        config,
        Some(requirements),
        &permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()),
    )
    .expect("managed-only allowlist should still load");

    assert_eq!(
        spec.config.network.allowed_domains(),
        Some(vec!["managed.example.com".to_string()])
    );
    assert_eq!(
        spec.constraints.allowed_domains,
        Some(vec!["managed.example.com".to_string()])
    );
    assert_eq!(spec.constraints.allowlist_expansion_enabled, Some(false));
    assert!(spec.hard_deny_allowlist_misses);
}

#[test]
fn managed_allowed_domains_only_without_managed_allowlist_blocks_all_user_domains() {
    let mut config = NetworkProxyConfig::default();
    config
        .network
        .set_allowed_domains(vec!["api.example.com".to_string()]);
    let requirements = NetworkConstraints {
        managed_allowed_domains_only: Some(true),
        ..Default::default()
    };

    let spec = NetworkProxySpec::from_config_and_constraints(
        config,
        Some(requirements),
        &permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()),
    )
    .expect("managed-only mode should treat missing managed allowlist as empty");

    assert_eq!(spec.config.network.allowed_domains(), None);
    assert_eq!(spec.constraints.allowed_domains, Some(Vec::new()));
    assert_eq!(spec.constraints.allowlist_expansion_enabled, Some(false));
    assert!(spec.hard_deny_allowlist_misses);
}

#[test]
fn managed_allowed_domains_only_blocks_all_user_domains_in_full_access_without_managed_list() {
    let mut config = NetworkProxyConfig::default();
    config
        .network
        .set_allowed_domains(vec!["api.example.com".to_string()]);
    let requirements = NetworkConstraints {
        managed_allowed_domains_only: Some(true),
        ..Default::default()
    };

    let spec = NetworkProxySpec::from_config_and_constraints(
        config,
        Some(requirements),
        &permission_profile_for_sandbox_policy(&SandboxPolicy::DangerFullAccess),
    )
    .expect("managed-only mode should treat missing managed allowlist as empty");

    assert_eq!(spec.config.network.allowed_domains(), None);
    assert_eq!(spec.constraints.allowed_domains, Some(Vec::new()));
    assert_eq!(spec.constraints.allowlist_expansion_enabled, Some(false));
    assert!(spec.hard_deny_allowlist_misses);
}

#[test]
fn deny_only_requirements_do_not_create_allow_constraints_in_full_access() {
    let mut config = NetworkProxyConfig::default();
    config
        .network
        .set_allowed_domains(vec!["api.example.com".to_string()]);
    let requirements = NetworkConstraints {
        domains: Some(domain_permissions([(
            "managed-blocked.example.com",
            NetworkDomainPermissionToml::Deny,
        )])),
        ..Default::default()
    };

    let spec = NetworkProxySpec::from_config_and_constraints(
        config,
        Some(requirements),
        &permission_profile_for_sandbox_policy(&SandboxPolicy::DangerFullAccess),
    )
    .expect("deny-only requirements should not constrain the allowlist");

    assert_eq!(
        spec.config.network.allowed_domains(),
        Some(vec!["api.example.com".to_string()])
    );
    assert_eq!(spec.constraints.allowed_domains, None);
    assert_eq!(spec.constraints.allowlist_expansion_enabled, None);
    assert_eq!(
        spec.config.network.denied_domains(),
        Some(vec!["managed-blocked.example.com".to_string()])
    );
}

#[test]
fn allow_only_requirements_do_not_create_deny_constraints_in_full_access() {
    let mut config = NetworkProxyConfig::default();
    config
        .network
        .set_denied_domains(vec!["blocked.example.com".to_string()]);
    let requirements = NetworkConstraints {
        domains: Some(domain_permissions([(
            "managed.example.com",
            NetworkDomainPermissionToml::Allow,
        )])),
        ..Default::default()
    };

    let spec = NetworkProxySpec::from_config_and_constraints(
        config,
        Some(requirements),
        &permission_profile_for_sandbox_policy(&SandboxPolicy::DangerFullAccess),
    )
    .expect("allow-only requirements should not constrain the denylist");

    assert_eq!(
        spec.config.network.allowed_domains(),
        Some(vec!["managed.example.com".to_string()])
    );
    assert_eq!(
        spec.config.network.denied_domains(),
        Some(vec!["blocked.example.com".to_string()])
    );
    assert_eq!(spec.constraints.denied_domains, None);
    assert_eq!(spec.constraints.denylist_expansion_enabled, None);
}

#[test]
fn requirements_denied_domains_are_a_baseline_for_default_mode() {
    let mut config = NetworkProxyConfig::default();
    config
        .network
        .set_denied_domains(vec!["blocked.example.com".to_string()]);
    let requirements = NetworkConstraints {
        domains: Some(domain_permissions([(
            "managed-blocked.example.com",
            NetworkDomainPermissionToml::Deny,
        )])),
        ..Default::default()
    };

    let spec = NetworkProxySpec::from_config_and_constraints(
        config,
        Some(requirements),
        &permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()),
    )
    .expect("default mode should merge managed and user deny entries");

    assert_eq!(
        spec.config.network.denied_domains(),
        Some(vec![
            "managed-blocked.example.com".to_string(),
            "blocked.example.com".to_string()
        ])
    );
    assert_eq!(
        spec.constraints.denied_domains,
        Some(vec!["managed-blocked.example.com".to_string()])
    );
    assert_eq!(spec.constraints.denylist_expansion_enabled, Some(true));
}

#[test]
fn requirements_denylist_expansion_keeps_user_entries_mutable() {
    let mut config = NetworkProxyConfig::default();
    config
        .network
        .set_denied_domains(vec!["blocked.example.com".to_string()]);
    let requirements = NetworkConstraints {
        domains: Some(domain_permissions([(
            "managed-blocked.example.com",
            NetworkDomainPermissionToml::Deny,
        )])),
        ..Default::default()
    };

    let spec = NetworkProxySpec::from_config_and_constraints(
        config,
        Some(requirements),
        &permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()),
    )
    .expect("managed baseline should still allow user edits");

    let mut candidate = spec.config.clone();
    candidate.network.upsert_domain_permission(
        "blocked.example.com".to_string(),
        NetworkDomainPermission::Allow,
        normalize_host,
    );

    assert_eq!(
        candidate.network.allowed_domains(),
        Some(vec!["blocked.example.com".to_string()])
    );
    assert_eq!(
        candidate.network.denied_domains(),
        Some(vec!["managed-blocked.example.com".to_string()])
    );
    validate_policy_against_constraints(&candidate, &spec.constraints)
        .expect("user denylist entries should not become managed constraints");
}
