use super::*;
use coil_auth::DefaultAuthModelPackage;
use coil_cache::DistributedCacheBackend;
use coil_config::PlatformConfig;
use coil_i18n::MessageKey;
use coil_template::TemplateNamespace;
use coil_wasm::ExtensionPointKind;

const VALID_CONFIG: &str = r#"
[app]
name = "showcase-events"
environment = "production"

[server]
bind = "0.0.0.0:8080"
trusted_proxies = ["10.0.0.0/8"]

[http.session]
store = "redis"
idle_timeout_secs = 3600
absolute_timeout_secs = 86400

[http.session_cookie]
name = "coil_session"
path = "/"
same_site = "lax"
secure = true
http_only = true

[http.flash_cookie]
name = "coil_flash"
path = "/"
same_site = "lax"
secure = true
http_only = true

[http.csrf]
enabled = true
field_name = "_csrf"
header_name = "x-csrf-token"

[tls]
mode = "acme"
challenge = "dns-01"
provider = "cloudflare-dns"

[storage]
default_class = "public_upload"
object_store = "s3"
local_root = "/var/lib/coil"

[cache]
l1 = "moka"
l2 = "redis"

[i18n]
default_locale = "en-GB"
supported_locales = ["en-GB", "fr-FR"]
fallback_locale = "en-GB"

[seo]
canonical_host = "www.example.com"
emit_json_ld = true

[auth]
package = "coil-default-auth"
explain_api = false
tenant_id = 101

[modules]
enabled = ["cms-pages", "admin-shell"]

[wasm]
directory = "extensions"
default_time_limit_ms = 50
allow_network = false

[jobs]
backend = "redis"

[observability]
metrics = true
tracing = true

[assets]
publish_manifest = true
cdn_base_url = "https://cdn.example.com"
"#;

#[test]
fn bootstrap_registers_core_services() {
    let config = PlatformConfig::from_toml_str(VALID_CONFIG).unwrap();
    let bootstrap = bootstrap_core_services(&config).unwrap();

    let ids = bootstrap
        .registry
        .services()
        .map(|service| service.id.as_str())
        .collect::<Vec<_>>();

    assert!(ids.contains(&"core.config"));
    assert!(ids.contains(&"core.cli"));
    assert!(ids.contains(&"core.auth"));
    assert!(ids.contains(&"core.tls"));
    assert!(ids.contains(&"core.tls.reload"));
    assert!(ids.contains(&"core.data"));
    assert!(ids.contains(&"core.data.migrations"));
    assert!(ids.contains(&"core.jobs"));
    assert!(ids.contains(&"core.health"));
    assert!(ids.contains(&"core.maintenance"));
    assert!(ids.contains(&"core.flags"));
    assert!(ids.contains(&"core.metrics"));
    assert!(ids.contains(&"core.cache.l1"));
    assert!(ids.contains(&"core.cache.l2"));
    assert!(ids.contains(&"core.cache.invalidation"));
    assert!(ids.contains(&"core.cache.http"));
    assert!(ids.contains(&"core.http"));
    assert!(ids.contains(&"core.http.sessions"));
    assert!(ids.contains(&"core.http.cookies"));
    assert!(ids.contains(&"core.http.csrf"));
    assert!(ids.contains(&"core.i18n"));
    assert!(ids.contains(&"core.seo"));
    assert!(ids.contains(&"core.a11y"));
    assert!(ids.contains(&"core.template.fragments"));
    assert!(ids.contains(&"core.wasm"));
    assert!(ids.contains(&"core.wasm.limits"));
    assert!(ids.contains(&"core.customer_plugins"));
    assert_eq!(
        bootstrap.cache.distributed_backend(),
        Some(DistributedCacheBackend::Redis)
    );
    assert!(bootstrap.cache.shared_invalidation_enabled());
    assert_eq!(
        bootstrap.browser.sessions.store,
        SessionStoreTopology::Redis
    );
    assert_eq!(
        bootstrap.browser.sessions.idle_timeout,
        Duration::from_secs(3600)
    );
    assert_eq!(
        bootstrap.browser.sessions.session_cookie.name,
        "coil_session"
    );
    assert_eq!(bootstrap.browser.csrf.field_name, "_csrf");
    assert!(bootstrap.cli.customer_app == "showcase-events");
    assert_eq!(bootstrap.cli.baseline_command_count, 4);
    assert_eq!(
        bootstrap.data.driver,
        coil_config::DatabaseDriver::Postgres
    );
    assert_eq!(bootstrap.data.schema, "public");
    assert_eq!(bootstrap.data.migrations_table, "_coil_migrations");
    assert_eq!(bootstrap.jobs.backend, coil_config::JobBackend::Redis);
    assert_eq!(bootstrap.jobs.topology.work_queue.as_str(), "jobs.work");
    assert_eq!(
        bootstrap.jobs.topology.domain_events_queue.as_str(),
        "jobs.domain-events"
    );
    assert_eq!(bootstrap.tls.mode, TlsMode::Acme);
    assert_eq!(
        bootstrap.tls.provider,
        Some(coil_tls::CertificateProviderKind::CloudflareDns)
    );
    assert_eq!(
        bootstrap.tls.challenge,
        Some(coil_tls::ChallengeStrategy::Dns01)
    );
    assert!(bootstrap.tls.hot_reload_supported);
    assert!(bootstrap.observability.telemetry.metrics_enabled);
    assert!(bootstrap.observability.telemetry.trace.enabled);
    assert!(
        bootstrap
            .observability
            .readiness
            .dependencies
            .iter()
            .any(|dependency| dependency.kind == DependencyKind::Database)
    );
    assert!(
        bootstrap
            .observability
            .readiness
            .dependencies
            .iter()
            .any(|dependency| dependency.kind == DependencyKind::DistributedCache)
    );
    assert!(
        bootstrap
            .observability
            .readiness
            .dependencies
            .iter()
            .any(|dependency| dependency.kind == DependencyKind::Queue)
    );
    assert!(
        bootstrap
            .observability
            .readiness
            .dependencies
            .iter()
            .any(|dependency| dependency.kind == DependencyKind::ObjectStore)
    );
    assert!(
        bootstrap
            .observability
            .readiness
            .dependencies
            .iter()
            .any(|dependency| dependency.kind == DependencyKind::Secrets)
    );
    assert!(
        bootstrap
            .observability
            .readiness
            .dependencies
            .iter()
            .any(|dependency| dependency.kind == DependencyKind::Tls)
    );
    let locale_context = bootstrap.i18n.request_context(Some("fr-FR"));
    assert_eq!(locale_context.locale.as_str(), "fr-FR");
    assert_eq!(locale_context.currency.as_str(), "EUR");
    assert_eq!(locale_context.timezone.as_str(), "Europe/Paris");
    assert_eq!(
        bootstrap
            .i18n
            .router
            .absolute_url(&bootstrap.i18n.default_locale, "/events")
            .unwrap(),
        "https://www.example.com/en-GB/events"
    );
    assert!(bootstrap.seo.emit_json_ld);
    assert!(bootstrap.seo.sitemap_enabled);
    assert_eq!(bootstrap.a11y.navigation.skip_link_target, "main-content");
    assert!(bootstrap.a11y.theme_baseline.meets_platform_baseline());
    assert_eq!(
        bootstrap
            .template
            .namespace_chain(Some(&TemplateNamespace::new("events").unwrap())),
        vec![
            TemplateNamespace::new("customer-app").unwrap(),
            TemplateNamespace::new("events").unwrap(),
            TemplateNamespace::new("core").unwrap(),
        ]
    );
    assert_eq!(bootstrap.wasm.extension_directory, "extensions");
    assert!(!bootstrap.wasm.allow_network);
    assert_eq!(
        bootstrap
            .wasm
            .limits
            .for_point(ExtensionPointKind::Page)
            .max_runtime,
        Duration::from_millis(50)
    );
    assert_eq!(
        bootstrap
            .wasm
            .limits
            .for_point(ExtensionPointKind::Job)
            .max_runtime,
        Duration::from_secs(30)
    );
}

#[test]
fn validates_module_capabilities_against_auth_package() {
    let package = DefaultAuthModelPackage::default();
    let manifest = ModuleManifest::new("cms-pages")
        .with_required_capabilities(vec![Capability::CmsPageRead, Capability::CmsPagePublish])
        .with_capability_contracts(vec![
            CapabilityContract::required(Capability::CmsPageRead, ["page"]),
            CapabilityContract::required(Capability::CmsPagePublish, ["page"]),
        ])
        .with_jobs(vec![JobContract::new(
            "cms.publish-scheduled",
            JobTriggerKind::Scheduled,
            true,
            "Publishes scheduled pages",
        )])
        .with_event_subscriptions(vec![EventSubscription::new(
            "cms.page.publish-requested",
            Some("cms.publish-scheduled"),
            "Schedules page publication",
        )]);

    assert!(validate_module_capabilities(&package, &manifest).is_ok());

    let invalid = ModuleManifest::new("cms-pages")
        .with_required_capabilities(vec![Capability::CmsPageRead])
        .with_capability_contracts(vec![CapabilityContract::required(
            Capability::CmsPageRead,
            ["page"],
        )])
        .with_event_subscriptions(vec![EventSubscription::new(
            "cms.page.publish-requested",
            Some("cms.publish-scheduled"),
            "Schedules page publication",
        )]);
    assert_eq!(
        validate_module_capabilities(&package, &invalid).unwrap_err(),
        CapabilityValidationError::UnknownSubscriptionJob {
            module: "cms-pages".to_string(),
            event: "cms.page.publish-requested".to_string(),
            job: "cms.publish-scheduled".to_string(),
        }
    );

    let invalid_search = ModuleManifest::new("search")
        .with_optional_capabilities(vec![Capability::CmsPageRead])
        .with_capability_contracts(vec![CapabilityContract::optional(
            Capability::CmsPageRead,
            ["page"],
        )])
        .with_search_contributions(vec![SearchIndexContribution::new(
            "search.pages",
            SearchDocumentKind::Page,
            SearchVisibility::Public,
            false,
            vec![SearchFieldContribution::new(
                "title",
                "title",
                SearchFieldRole::Title,
                true,
                true,
            )],
            vec![SearchInvalidationRule::new(
                SearchInvalidationTrigger::Published,
                "page published",
            )],
            SearchRebuildStrategy::OnInvalidate,
        )]);
    assert_eq!(
        validate_module_capabilities(&package, &invalid_search).unwrap_err(),
        CapabilityValidationError::InvalidOperationalContribution {
            module: "search".to_string(),
            kind: "search contribution",
            id: "search.pages".to_string(),
            reason: "public search indexes must require publication state".to_string(),
        }
    );
}

#[test]
fn bootstrap_core_services_merges_customer_translation_catalogs() {
    let config = PlatformConfig::from_toml_str(VALID_CONFIG).unwrap();
    let bootstrap = bootstrap_core_services_with_translation_catalogs(
        &config,
        vec![TranslationCatalog::new(
            LocaleTag::new("fr-FR").unwrap(),
            vec![(
                MessageKey::new("checkout.title").unwrap(),
                "Paiement".to_string(),
            )],
        )
        .unwrap()],
    )
    .unwrap();

    let context = bootstrap.i18n.request_context(Some("fr-FR"));
    assert_eq!(
        bootstrap
            .i18n
            .translations
            .translate(&context, &MessageKey::new("checkout.title").unwrap())
            .unwrap(),
        "Paiement"
    );
    assert_eq!(
        bootstrap
            .i18n
            .translations
            .translate(&context, &MessageKey::new("core.locale").unwrap())
            .unwrap(),
        "fr-FR"
    );
}

#[test]
fn validates_module_installation_dependencies_against_installed_modules_and_core_services() {
    let manifest = ModuleManifest::new("memberships")
        .with_module_dependencies(vec![ModuleDependency::required(
            "commerce",
            "subscription purchases depend on order outcomes",
        )])
        .with_core_service_dependencies(vec![
            CoreServiceDependency::Auth,
            CoreServiceDependency::Data,
            CoreServiceDependency::Jobs,
        ]);

    let missing_dependency = validate_module_installation(
        &manifest,
        &["cms".to_string()],
        &[
            "core.auth",
            "core.data",
            "core.data.migrations",
            "core.jobs",
        ],
    )
    .unwrap_err();
    assert_eq!(
        missing_dependency,
        ModuleInstallationError::MissingModuleDependency {
            module: "memberships".to_string(),
            dependency: "commerce".to_string(),
        }
    );

    assert!(
        validate_module_installation(
            &manifest,
            &["commerce".to_string(), "memberships".to_string()],
            &[
                "core.auth",
                "core.data",
                "core.data.migrations",
                "core.jobs"
            ],
        )
        .is_ok()
    );
}

#[test]
fn signed_cookie_round_trips_and_rejects_tampering() {
    let config = PlatformConfig::from_toml_str(VALID_CONFIG).unwrap();
    let bootstrap = bootstrap_core_services(&config).unwrap();
    let signer = CookieSigner::new(bootstrap.browser.sessions.session_cookie.clone());
    let secret = b"0123456789abcdef0123456789abcdef";

    let signed = signer.sign(secret, "sess_123").unwrap();
    assert_eq!(signer.verify(secret, &signed).unwrap(), "sess_123");

    let mut tampered = signed.clone();
    let last = tampered.pop().unwrap();
    tampered.push(if last == 'A' { 'B' } else { 'A' });
    assert!(signer.verify(secret, &tampered).is_err());
}

#[test]
fn encrypted_cookie_round_trips_and_rejects_tampering() {
    let config = PlatformConfig::from_toml_str(VALID_CONFIG).unwrap();
    let bootstrap = bootstrap_core_services(&config).unwrap();
    let sealer = CookieSealer::new(CookiePolicy {
        protection: CookieProtection::Encrypted,
        ..bootstrap.browser.sessions.flash_cookie.clone()
    });
    let secret = b"fedcba9876543210fedcba9876543210";

    let sealed = sealer.seal(secret, "flash:welcome-back").unwrap();
    assert_eq!(sealer.open(secret, &sealed).unwrap(), "flash:welcome-back");

    let mut tampered = sealed.clone();
    let last = tampered.pop().unwrap();
    tampered.push(if last == 'A' { 'B' } else { 'A' });
    assert!(sealer.open(secret, &tampered).is_err());
}

#[test]
fn cookie_primitives_enforce_declared_protection_mode() {
    let config = PlatformConfig::from_toml_str(VALID_CONFIG).unwrap();
    let bootstrap = bootstrap_core_services(&config).unwrap();
    let signed = bootstrap.browser.sessions.session_cookie.clone();
    let encrypted = CookiePolicy {
        protection: CookieProtection::Encrypted,
        ..bootstrap.browser.sessions.flash_cookie.clone()
    };
    let secret = b"0123456789abcdef0123456789abcdef";

    assert_eq!(
        CookieSigner::new(encrypted.clone())
            .sign(secret, "value")
            .unwrap_err(),
        BrowserSecurityError::UnexpectedCookieProtection {
            expected: CookieProtection::Signed,
            actual: CookieProtection::Encrypted,
        }
    );
    assert_eq!(
        CookieSealer::new(signed).seal(secret, "value").unwrap_err(),
        BrowserSecurityError::UnexpectedCookieProtection {
            expected: CookieProtection::Encrypted,
            actual: CookieProtection::Signed,
        }
    );
}

#[test]
fn csrf_tokens_bind_to_session_and_action() {
    let config = PlatformConfig::from_toml_str(VALID_CONFIG).unwrap();
    let bootstrap = bootstrap_core_services(&config).unwrap();
    let secret = b"abcdef0123456789abcdef0123456789";

    let token = bootstrap
        .browser
        .csrf
        .issue_token(secret, "sess_123", "/checkout")
        .unwrap();

    assert!(
        bootstrap
            .browser
            .csrf
            .verify_token(secret, "sess_123", "/checkout", &token)
            .unwrap()
    );
    assert!(
        !bootstrap
            .browser
            .csrf
            .verify_token(secret, "sess_999", "/checkout", &token)
            .unwrap()
    );
}
