// Shared test harness for E2E / integration tests.
//
// Spins up the agent API server on a random port with an isolated test
// database. Each test gets a fresh database that is dropped on cleanup.
//
// Included via `mod common;` in several test binaries; not every binary uses
// every helper, so allow dead code here.
#![allow(dead_code)]

use std::sync::Arc;

use compliance_agent::agent::ComplianceAgent;
use compliance_agent::api;
use compliance_agent::database::DatabasePool;
use compliance_core::AgentConfig;
use secrecy::SecretString;

/// The runner bearer token wired into the test config.
pub const TEST_RUNNER_TOKEN: &str = "test-runner-token";

/// A minimal dev [`AgentConfig`] for tests: unauthenticated (no Keycloak), the
/// Werkbank runner API enabled with [`TEST_RUNNER_TOKEN`].
pub fn dev_config(mongodb_uri: String, db_name: String) -> AgentConfig {
    AgentConfig {
        mongodb_uri,
        mongodb_database: db_name,
        litellm_url: std::env::var("TEST_LITELLM_URL")
            .unwrap_or_else(|_| "http://localhost:4000".into()),
        litellm_api_key: SecretString::from(String::new()),
        litellm_model: "gpt-4o".into(),
        litellm_embed_model: "text-embedding-3-small".into(),
        agent_port: 0, // not used — we bind ourselves
        scan_schedule: String::new(),
        cve_monitor_schedule: String::new(),
        git_clone_base_path: "/tmp/compliance-scanner-tests/repos".into(),
        artifact_store_base_path: "/tmp/compliance-scanner-tests/artifacts".into(),
        ssh_key_path: "/tmp/compliance-scanner-tests/ssh/id_ed25519".into(),
        github_token: None,
        github_webhook_secret: None,
        gitlab_url: None,
        gitlab_token: None,
        gitlab_webhook_secret: None,
        jira_url: None,
        jira_email: None,
        jira_api_token: None,
        jira_project_key: None,
        searxng_url: None,
        nvd_api_key: None,
        keycloak_url: None,
        keycloak_realm: None,
        keycloak_admin_username: None,
        keycloak_admin_password: None,
        pentest_verification_email: None,
        pentest_imap_host: None,
        pentest_imap_port: None,
        pentest_imap_tls: false,
        pentest_imap_username: None,
        pentest_imap_password: None,
        admin_api_token: None,
        tenant_registry_url: None,
        plc_runtime: compliance_core::PlcRuntimeConfig::default(),
        werkbank_runner_token: Some(SecretString::from(TEST_RUNNER_TOKEN.to_string())),
        breakpilot: compliance_core::config::BreakpilotConfig::default(),
    }
}

/// A running test server with a unique database.
pub struct TestServer {
    pub base_url: String,
    pub client: reqwest::Client,
    db_name: String,
    mongodb_uri: String,
}

impl TestServer {
    /// Start an agent API server on a random port with an isolated database.
    pub async fn start() -> Self {
        let mongodb_uri = std::env::var("TEST_MONGODB_URI")
            .unwrap_or_else(|_| "mongodb://root:example@localhost:27017/?authSource=admin".into());

        // Unique db-name prefix per run. Must fit the pool's 30-char cap
        // (`<prefix>_<32 hex>` <= 63), so use a 16-hex-char suffix.
        let db_name = format!("t_{}", &uuid::Uuid::new_v4().simple().to_string()[..16]);

        let db_pool = DatabasePool::connect(&mongodb_uri, &db_name)
            .await
            .expect("Failed to build DatabasePool");

        let config = dev_config(mongodb_uri.clone(), db_name.clone());

        let agent = ComplianceAgent::new(config, db_pool);

        // Build the router with the agent extension. After M7.2-B every
        // handler takes a TenantCtx extractor; without KC in the test
        // harness, the dev-tenant injector mounts a synthetic context so
        // tests run end-to-end against `<db_name>_dev`.
        let app = api::routes::build_router()
            .layer(axum::extract::Extension(Arc::new(agent)))
            .layer(axum::middleware::from_fn(api::server::inject_dev_tenant))
            .layer(tower_http::cors::CorsLayer::permissive());

        // Bind to port 0 to get a random available port
        let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
            .await
            .expect("Failed to bind test server");
        let port = listener.local_addr().expect("no local addr").port();

        tokio::spawn(async move {
            axum::serve(listener, app).await.ok();
        });

        let base_url = format!("http://127.0.0.1:{port}");
        let client = reqwest::Client::builder()
            .timeout(std::time::Duration::from_secs(30))
            .build()
            .expect("Failed to build HTTP client");

        // Wait for server to be ready
        for _ in 0..50 {
            if client
                .get(format!("{base_url}/api/v1/health"))
                .send()
                .await
                .is_ok()
            {
                break;
            }
            tokio::time::sleep(std::time::Duration::from_millis(50)).await;
        }

        Self {
            base_url,
            client,
            db_name,
            mongodb_uri,
        }
    }

    /// GET helper
    pub async fn get(&self, path: &str) -> reqwest::Response {
        self.client
            .get(format!("{}{path}", self.base_url))
            .send()
            .await
            .expect("GET request failed")
    }

    /// POST helper with JSON body
    pub async fn post(&self, path: &str, body: &serde_json::Value) -> reqwest::Response {
        self.client
            .post(format!("{}{path}", self.base_url))
            .json(body)
            .send()
            .await
            .expect("POST request failed")
    }

    /// PATCH helper with JSON body
    pub async fn patch(&self, path: &str, body: &serde_json::Value) -> reqwest::Response {
        self.client
            .patch(format!("{}{path}", self.base_url))
            .json(body)
            .send()
            .await
            .expect("PATCH request failed")
    }

    /// DELETE helper
    pub async fn delete(&self, path: &str) -> reqwest::Response {
        self.client
            .delete(format!("{}{path}", self.base_url))
            .send()
            .await
            .expect("DELETE request failed")
    }

    /// Get the unique database name for direct MongoDB access in tests.
    pub fn db_name(&self) -> &str {
        &self.db_name
    }

    /// Drop every per-tenant database belonging to this test run.
    /// Post-M7.2-D the agent never opens a `db_name` directly —
    /// data lives only in `<db_name>_<tenant>` per-tenant databases.
    pub async fn cleanup(&self) {
        if let Ok(client) = mongodb::Client::with_uri_str(&self.mongodb_uri).await {
            if let Ok(names) = client.list_database_names().await {
                let prefix = format!("{}_", self.db_name);
                for name in names {
                    if name.starts_with(&prefix) {
                        client.database(&name).drop().await.ok();
                    }
                }
            }
        }
    }
}
