//! End-to-end integration test.
//! Requires `TEST_DATABASE_URL` to be set.
//! Tests the full flow: create project -> jobset -> evaluation -> builds.
//!
//! Nix-dependent steps are skipped if nix is not available.
#![expect(
  clippy::unwrap_used,
  clippy::expect_used,
  clippy::print_stdout,
  reason = "Fine in tests"
)]

use axum::{
  body::Body,
  http::{Request, StatusCode},
};
use circus_common::models::*;
use tower::ServiceExt;

async fn get_pool() -> Option<sqlx::PgPool> {
  let Ok(url) = std::env::var("TEST_DATABASE_URL") else {
    println!("Skipping E2E test: TEST_DATABASE_URL not set");
    return None;
  };

  let pool = sqlx::postgres::PgPoolOptions::new()
    .max_connections(5)
    .connect(&url)
    .await
    .ok()?;

  sqlx::migrate!("../common/migrations")
    .run(&pool)
    .await
    .ok()?;

  Some(pool)
}

#[tokio::test]
async fn test_e2e_project_eval_build_flow() {
  let Some(pool) = get_pool().await else {
    return;
  };

  // 1. Create a project
  let project_name = format!("e2e-test-{}", uuid::Uuid::new_v4());
  let project = circus_common::repo::projects::create(&pool, CreateProject {
    name:            project_name.clone(),
    description:     Some("E2E test project".to_string()),
    repository_url:  "https://github.com/test/e2e".to_string(),
    cache_enabled:   true,
    cache_url:       None,
    cache_upstreams: BinaryCacheUpstreams::default(),
  })
  .await
  .expect("create project");

  assert_eq!(project.name, project_name);

  // 2. Create a jobset
  let jobset = circus_common::repo::jobsets::create(&pool, CreateJobset {
    project_id:        project.id,
    name:              "default".to_string(),
    nix_expression:    "packages".to_string(),
    enabled:           Some(true),
    flake_mode:        Some(true),
    check_interval:    Some(300),
    trigger_mode:      None,
    branch:            None,
    branch_pattern:    None,
    tag_pattern:       None,
    scheduling_shares: None,
    state:             None,
    keep_nr:           None,
  })
  .await
  .expect("create jobset");

  assert_eq!(jobset.project_id, project.id);
  assert!(jobset.enabled);

  // 3. Verify active jobsets include our new one
  let active = circus_common::repo::jobsets::list_active(&pool)
    .await
    .expect("list active");
  assert!(
    active.iter().any(|j| j.id == jobset.id),
    "new jobset should be in active list"
  );

  // 4. Create an evaluation
  let eval =
    circus_common::repo::evaluations::create(&pool, CreateEvaluation {
      jobset_id:      jobset.id,
      commit_hash:    "e2e0000000000000000000000000000000000000".to_string(),
      pr_number:      None,
      pr_head_branch: None,
      pr_base_branch: None,
      pr_action:      None,
    })
    .await
    .expect("create evaluation");

  assert_eq!(eval.jobset_id, jobset.id);
  assert_eq!(eval.status, EvaluationStatus::Pending);

  // 5. Mark evaluation as running
  circus_common::repo::evaluations::update_status(
    &pool,
    eval.id,
    EvaluationStatus::Running,
    None,
  )
  .await
  .expect("update eval status");

  // 6. Create builds as if nix evaluation found jobs
  let build1 = circus_common::repo::builds::create(&pool, CreateBuild {
    evaluation_id: eval.id,
    job_name: "hello".to_string(),
    drv_path: "/nix/store/e2e000-hello.drv".to_string(),
    system: Some("x86_64-linux".to_string()),
    outputs: Some(serde_json::json!({"out": "/nix/store/e2e000-hello"})),
    is_aggregate: Some(false),
    constituents: None,
    is_fod: None,
    fod_hash: None,
    ..Default::default()
  })
  .await
  .expect("create build 1");

  let build2 = circus_common::repo::builds::create(&pool, CreateBuild {
    evaluation_id: eval.id,
    job_name: "world".to_string(),
    drv_path: "/nix/store/e2e000-world.drv".to_string(),
    system: Some("x86_64-linux".to_string()),
    outputs: Some(serde_json::json!({"out": "/nix/store/e2e000-world"})),
    is_aggregate: Some(false),
    constituents: None,
    is_fod: None,
    fod_hash: None,
    ..Default::default()
  })
  .await
  .expect("create build 2");

  assert_eq!(build1.status, BuildStatus::Pending);
  assert_eq!(build2.status, BuildStatus::Pending);

  // 7. Create build dependency (hello depends on world)
  circus_common::repo::build_dependencies::create(&pool, build1.id, build2.id)
    .await
    .expect("create dependency");

  // 8. Verify dependency check: build1 deps NOT complete (world is still
  //    pending)
  let deps_complete =
    circus_common::repo::build_dependencies::all_deps_completed(
      &pool, build1.id,
    )
    .await
    .expect("check deps");
  assert!(!deps_complete, "deps should NOT be complete yet");

  // 9. Complete build2 (world)
  circus_common::repo::builds::start(&pool, build2.id)
    .await
    .expect("start build2");
  circus_common::repo::builds::complete(
    &pool,
    build2.id,
    BuildStatus::Succeeded,
    None,
    Some("/nix/store/e2e000-world"),
    None,
  )
  .await
  .expect("complete build2");

  // 10. Now build1 deps should be complete
  let deps_complete =
    circus_common::repo::build_dependencies::all_deps_completed(
      &pool, build1.id,
    )
    .await
    .expect("check deps again");
  assert!(deps_complete, "deps should be complete after build2 done");

  // 11. Complete build1 (hello)
  circus_common::repo::builds::start(&pool, build1.id)
    .await
    .expect("start build1");

  let step = circus_common::repo::build_steps::create(&pool, CreateBuildStep {
    build_id:    build1.id,
    step_number: 1,
    command:     "nix build /nix/store/e2e000-hello.drv".to_string(),
  })
  .await
  .expect("create step");

  circus_common::repo::build_steps::complete(
    &pool,
    step.id,
    0,
    Some("built!"),
    None,
  )
  .await
  .expect("complete step");

  circus_common::repo::build_products::create(&pool, CreateBuildProduct {
    build_id:     build1.id,
    name:         "out".to_string(),
    path:         "/nix/store/e2e000-hello".to_string(),
    sha256_hash:  Some("abcdef1234567890".to_string()),
    file_size:    Some(12345),
    content_type: None,
    is_directory: true,
  })
  .await
  .expect("create product");

  circus_common::repo::builds::complete(
    &pool,
    build1.id,
    BuildStatus::Succeeded,
    None,
    Some("/nix/store/e2e000-hello"),
    None,
  )
  .await
  .expect("complete build1");

  // 12. Mark evaluation as completed
  circus_common::repo::evaluations::update_status(
    &pool,
    eval.id,
    EvaluationStatus::Completed,
    None,
  )
  .await
  .expect("complete eval");

  // 13. Verify everything is in the expected state
  let final_eval = circus_common::repo::evaluations::get(&pool, eval.id)
    .await
    .expect("get eval");
  assert_eq!(final_eval.status, EvaluationStatus::Completed);

  let final_build1 = circus_common::repo::builds::get(&pool, build1.id)
    .await
    .expect("get build1");
  assert_eq!(final_build1.status, BuildStatus::Succeeded);
  assert_eq!(
    final_build1.build_output_path.as_deref(),
    Some("/nix/store/e2e000-hello")
  );

  let products =
    circus_common::repo::build_products::list_for_build(&pool, build1.id)
      .await
      .expect("list products");
  assert_eq!(products.len(), 1);
  assert_eq!(products[0].name, "out");

  let steps =
    circus_common::repo::build_steps::list_for_build(&pool, build1.id)
      .await
      .expect("list steps");
  assert_eq!(steps.len(), 1);
  assert_eq!(steps[0].exit_code, Some(0));

  // 14. Verify build stats reflect our changes
  let build_stats = circus_common::repo::builds::get_stats(&pool)
    .await
    .expect("get stats");
  assert!(build_stats.completed_builds.unwrap_or(0) >= 2);

  // 15. Create a channel and verify it works
  let channel = circus_common::repo::channels::create(&pool, CreateChannel {
    project_id: project.id,
    name:       "stable".to_string(),
    jobset_id:  jobset.id,
  })
  .await
  .expect("create channel");

  let channels = circus_common::repo::channels::list_all(&pool)
    .await
    .expect("list channels");
  assert!(channels.iter().any(|c| c.id == channel.id));

  // 16. Test the HTTP API layer
  let config = circus_config::Config::default();
  let state = circus_server::state::AppState {
    pool:             pool.clone(),
    nix_store:        circus_server::state::NixStore::new(
      config.nix.store_dir.clone(),
    )
    .unwrap(),
    config:           config.clone(),
    sessions:         std::sync::Arc::new(dashmap::DashMap::new()),
    narinfo_cache:    circus_server::state::AppState::new_narinfo_cache(),
    http_client:      reqwest::Client::new(),
    csrf_secret:      std::sync::Arc::new([0u8; 32]),
    email_regex:      None,
    cache_traffic:    std::sync::Arc::new(dashmap::DashMap::new()),
    cache_public_key: None,
  };
  let app = circus_server::routes::router(state, &config);

  // GET /health
  let resp = app
    .clone()
    .oneshot(
      Request::builder()
        .uri("/health")
        .body(Body::empty())
        .unwrap(),
    )
    .await
    .unwrap();
  assert_eq!(resp.status(), StatusCode::OK);

  // GET /api/v1/projects/{id}
  let resp = app
    .clone()
    .oneshot(
      Request::builder()
        .uri(format!("/api/v1/projects/{}", project.id))
        .body(Body::empty())
        .unwrap(),
    )
    .await
    .unwrap();
  assert_eq!(resp.status(), StatusCode::OK);

  // GET /api/v1/builds/{id}
  let resp = app
    .clone()
    .oneshot(
      Request::builder()
        .uri(format!("/api/v1/builds/{}", build1.id))
        .body(Body::empty())
        .unwrap(),
    )
    .await
    .unwrap();
  assert_eq!(resp.status(), StatusCode::OK);

  // GET / (dashboard)
  let resp = app
    .clone()
    .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
    .await
    .unwrap();
  assert_eq!(resp.status(), StatusCode::OK);
  let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
    .await
    .unwrap();
  let body_str = String::from_utf8(body.to_vec()).unwrap();
  assert!(body_str.contains("Dashboard"));

  // Clean up
  let _ = circus_common::repo::projects::delete(&pool, project.id).await;
}
