//! CLI integration tests
//!
//! These tests exercise the new keg CLI commands.
//! `set_current_dir` is process-global, so all tests that change the cwd
//! MUST run with `--test-threads=1`:
//!   cargo test -p kegani-cli --test cli_integration_tests -- --test-threads=1

use kegani_cli::GenKind;
use std::fs;
use std::path::PathBuf;

// ---------------------------------------------------------------------------
// WithTempDir guard
// ---------------------------------------------------------------------------

struct WithTempDir {
    _temp: tempfile::TempDir,
    _original: PathBuf,
}

impl WithTempDir {
    fn new() -> Self {
        let original = std::env::current_dir()
            .ok()
            .map(|p| p.canonicalize().unwrap_or(p))
            .unwrap_or_else(|| PathBuf::from("/"));

        let temp = tempfile::TempDir::new().expect("create temp dir");
        let tmp = temp.path().to_path_buf();
        let canonical_tmp = tmp.canonicalize().unwrap_or(tmp);
        std::env::set_current_dir(&canonical_tmp).expect("change dir to temp");

        Self {
            _temp: temp,
            _original: original,
        }
    }

    fn path(&self) -> PathBuf {
        self._temp
            .path()
            .canonicalize()
            .unwrap_or_else(|_| self._temp.path().to_path_buf())
    }
}

impl Drop for WithTempDir {
    fn drop(&mut self) {
        let _ = std::env::set_current_dir(&self._original);
    }
}

// ---------------------------------------------------------------------------
// `clean` tests (no cwd changes)
// ---------------------------------------------------------------------------

mod clean_tests {
    use super::*;

    #[test]
    fn test_clean_idempotent_when_no_target() {
        let result = kegani_cli::commands::clean::clean(false);
        assert!(result.is_ok(), "clean should not error when target absent");
    }

    #[test]
    fn test_clean_removes_target_dir() {
        let guard = WithTempDir::new();
        let tmp = guard.path();

        let fake_target = tmp.join("target");
        fs::create_dir_all(fake_target.join("debug")).expect("create fake target");
        fs::write(fake_target.join("debug").join("a.bin"), b"artifact").expect("write artifact");
        assert!(fake_target.exists());

        let result = kegani_cli::commands::clean::clean(false);
        assert!(result.is_ok());
        assert!(!fake_target.exists(), "target should be removed");
    }
}

// ---------------------------------------------------------------------------
// `init` tests
// ---------------------------------------------------------------------------

mod init_tests {
    use super::*;

    fn init_project(name: &str) -> (PathBuf, WithTempDir) {
        let guard = WithTempDir::new();
        let tmp = guard.path();

        kegani_cli::commands::init::init(name, "api").expect("init should succeed");

        (tmp.join(name), guard)
    }

    #[test]
    fn test_init_creates_project_directory() {
        let (project_dir, _guard) = init_project("my-api");
        assert!(project_dir.is_dir(), "project directory should exist");
    }

    #[test]
    fn test_init_writes_cargo_toml() {
        let (project_dir, _guard) = init_project("sample-app");
        let cargo = fs::read_to_string(project_dir.join("Cargo.toml")).unwrap();
        assert!(cargo.contains(r#"name = "sample-app""#));
        assert!(cargo.contains("kegani"));
        assert!(cargo.contains("actix-web"));
        assert!(cargo.contains("tokio"));
    }

    #[test]
    fn test_init_writes_main_rs() {
        let (project_dir, _guard) = init_project("hello-kegani");
        let main_rs = fs::read_to_string(project_dir.join("src/main.rs")).unwrap();
        assert!(main_rs.contains("#[tokio::main]"));
        assert!(main_rs.contains("tracing_subscriber"));
        assert!(main_rs.contains("App::new"));
    }

    #[test]
    fn test_init_writes_lib_rs() {
        let (project_dir, _guard) = init_project("lib-test");
        let lib = fs::read_to_string(project_dir.join("src/lib.rs")).unwrap();
        assert!(lib.contains("pub mod error"));
        assert!(lib.contains("pub mod routes"));
        assert!(lib.contains("pub mod controller"));
        assert!(lib.contains("pub mod middleware"));
    }

    #[test]
    fn test_init_writes_error_rs() {
        let (project_dir, _guard) = init_project("error-test");
        let error = fs::read_to_string(project_dir.join("src/error.rs")).unwrap();
        assert!(error.contains("struct AppError"));
        assert!(error.contains("impl ResponseError for AppError"));
    }

    #[test]
    fn test_init_writes_routes_mod() {
        let (project_dir, _guard) = init_project("routes-test");
        let routes = fs::read_to_string(project_dir.join("src/routes/mod.rs")).unwrap();
        assert!(routes.contains("pub mod health"));
        assert!(routes.contains("health::configure"));
    }

    #[test]
    fn test_init_writes_controller_api_mod() {
        let (project_dir, _guard) = init_project("ctrl-test");
        let ctrl = fs::read_to_string(project_dir.join("src/controller/api/mod.rs")).unwrap();
        assert!(ctrl.contains("keg gen api"));
    }

    #[test]
    fn test_init_writes_api_routes_mod() {
        let (project_dir, _guard) = init_project("api-routes-test");
        let routes = fs::read_to_string(project_dir.join("src/routes/api/mod.rs")).unwrap();
        assert!(routes.contains("keg gen api"));
    }

    #[test]
    fn test_init_creates_api_v1_dir() {
        let (project_dir, _guard) = init_project("v1-dir-test");
        assert!(project_dir.join("src/routes/api/v1").is_dir(), "src/routes/api/v1 dir should exist");
    }

    #[test]
    fn test_init_writes_manifest_config() {
        let (project_dir, _guard) = init_project("cfg-test");
        let config = fs::read_to_string(project_dir.join("manifest/config/config.yaml")).unwrap();
        assert!(config.contains("app:"));
        assert!(config.contains("database:"));
        assert!(config.contains("redis:"));
        assert!(config.contains("logging:"));
    }

    #[test]
    fn test_init_writes_dockerfile() {
        let (project_dir, _guard) = init_project("docker-test");
        let df = fs::read_to_string(project_dir.join("Dockerfile")).unwrap();
        assert!(df.contains("FROM rust:"));
        assert!(df.contains("WORKDIR /app"));
    }

    #[test]
    fn test_init_writes_docker_compose() {
        let (project_dir, _guard) = init_project("compose-test");
        let compose = fs::read_to_string(project_dir.join("docker-compose.yml")).unwrap();
        assert!(compose.contains("postgres:"));
        assert!(compose.contains("redis:"));
        assert!(compose.contains("services:"));
    }

    #[test]
    fn test_init_writes_gitignore() {
        let (project_dir, _guard) = init_project("git-test");
        let gitignore = fs::read_to_string(project_dir.join(".gitignore")).unwrap();
        assert!(gitignore.contains("/target/"));
        assert!(gitignore.contains(".env"));
    }

    #[test]
    fn test_init_writes_readme() {
        let (project_dir, _guard) = init_project("readme-test");
        let readme = fs::read_to_string(project_dir.join("README.md")).unwrap();
        assert!(readme.contains("readme-test"));
        assert!(readme.contains("Kegani"));
    }

    #[test]
    fn test_init_writes_env_example() {
        let (project_dir, _guard) = init_project("env-test");
        let env = fs::read_to_string(project_dir.join(".env.example")).unwrap();
        assert!(env.contains("DATABASE_URL"));
        assert!(env.contains("REDIS_URL"));
        assert!(env.contains("JWT_SECRET"));
    }

    #[test]
    fn test_init_creates_internal_layers() {
        let (project_dir, _guard) = init_project("layers-test");
        assert!(project_dir.join("internal/model/entity").is_dir(), "internal/model/entity should exist");
        assert!(project_dir.join("internal/dto").is_dir(), "internal/dto should exist");
        assert!(project_dir.join("internal/repository").is_dir(), "internal/repository should exist");
        assert!(project_dir.join("internal/logic").is_dir(), "internal/logic should exist");
        assert!(project_dir.join("internal/service").is_dir(), "internal/service should exist");
    }

    #[test]
    fn test_init_creates_resource_openapi() {
        let (project_dir, _guard) = init_project("openapi-test");
        assert!(project_dir.join("resource/openapi").is_dir(), "resource/openapi should exist");
        let schema = fs::read_to_string(project_dir.join("resource/openapi/schema.yaml")).unwrap();
        assert!(schema.contains("openapi:"));
    }

    #[test]
    fn test_init_writes_migrations_dir() {
        let (project_dir, _guard) = init_project("migrate-test");
        assert!(project_dir.join("migrations").is_dir(), "migrations dir should exist");
    }

    #[test]
    fn test_init_writes_middleware() {
        let (project_dir, _guard) = init_project("middleware-test");
        assert!(project_dir.join("src/middleware").is_dir(), "src/middleware should exist");
        let auth = fs::read_to_string(project_dir.join("src/middleware/auth.rs")).unwrap();
        assert!(auth.contains("JwtAuth"));
        assert!(auth.contains("extract_bearer_token"));
    }

    #[test]
    fn test_init_writes_health_routes() {
        let (project_dir, _guard) = init_project("health-test");
        let health = fs::read_to_string(project_dir.join("src/routes/health.rs")).unwrap();
        assert!(health.contains("/health"));
        assert!(health.contains("/health/live"));
        assert!(health.contains("/health/ready"));
    }

    #[test]
    fn test_init_writes_tests_dir() {
        let (project_dir, _guard) = init_project("tests-test");
        assert!(project_dir.join("tests").is_dir(), "tests dir should exist");
        assert!(project_dir.join("tests/api_test.rs").exists());
    }

    #[test]
    fn test_init_errors_on_non_empty_dir() {
        let guard = WithTempDir::new();
        let tmp = guard.path();

        let project_dir = tmp.join("existing");
        fs::create_dir_all(&project_dir).expect("create dir");
        fs::write(project_dir.join("README.md"), "existing content").expect("write file");

        let result = kegani_cli::commands::init::init("existing", "api");
        assert!(result.is_err(), "should error on non-empty directory");
    }
}

// ---------------------------------------------------------------------------
// `gen api` tests
// ---------------------------------------------------------------------------

mod gen_api_tests {
    use super::*;

    #[test]
    fn test_gen_api_creates_entity() {
        let guard = WithTempDir::new();
        let tmp = guard.path();

        // Create a basic project structure first
        kegani_cli::commands::init::init("test-proj", "api").expect("init");
        std::env::set_current_dir(&tmp.join("test-proj")).expect("change dir");

        // Generate api
        kegani_cli::commands::gen::gen(GenKind::Api, "article").expect("gen api");

        let entity = tmp.join("test-proj/internal/model/entity/article.rs");
        assert!(entity.exists(), "entity file should exist: {:?}", entity);
        let c = fs::read_to_string(&entity).unwrap();
        assert!(c.contains("pub struct Article"), "should have Article struct");
        assert!(c.contains("CreateArticle"), "should have CreateArticle");
        assert!(c.contains("UpdateArticle"), "should have UpdateArticle");
        assert!(c.contains("impl Article"), "should have impl");
    }

    #[test]
    fn test_gen_api_creates_repository() {
        let guard = WithTempDir::new();
        let tmp = guard.path();

        kegani_cli::commands::init::init("repo-test", "api").expect("init");
        std::env::set_current_dir(&tmp.join("repo-test")).expect("change dir");

        kegani_cli::commands::gen::gen(GenKind::Api, "post").expect("gen api");

        let repo = tmp.join("repo-test/internal/repository/post_repo.rs");
        assert!(repo.exists(), "repository file should exist: {:?}", repo);
        let c = fs::read_to_string(&repo).unwrap();
        assert!(c.contains("PostRepository"));
        assert!(c.contains("PgPool"));
        assert!(c.contains("pub async fn list"));
        assert!(c.contains("pub async fn find_by_id"));
    }

    #[test]
    fn test_gen_api_creates_logic() {
        let guard = WithTempDir::new();
        let tmp = guard.path();

        kegani_cli::commands::init::init("logic-test", "api").expect("init");
        std::env::set_current_dir(&tmp.join("logic-test")).expect("change dir");

        kegani_cli::commands::gen::gen(GenKind::Api, "user").expect("gen api");

        let logic = tmp.join("logic-test/internal/logic/user_logic.rs");
        assert!(logic.exists(), "logic file should exist: {:?}", logic);
        let c = fs::read_to_string(&logic).unwrap();
        assert!(c.contains("UserLogic"));
        assert!(c.contains("pub async fn list"));
        assert!(c.contains("pub async fn create"));
    }

    #[test]
    fn test_gen_api_creates_service() {
        let guard = WithTempDir::new();
        let tmp = guard.path();

        kegani_cli::commands::init::init("service-test", "api").expect("init");
        std::env::set_current_dir(&tmp.join("service-test")).expect("change dir");

        kegani_cli::commands::gen::gen(GenKind::Api, "order").expect("gen api");

        let service = tmp.join("service-test/internal/service/order_service.rs");
        assert!(service.exists(), "service file should exist: {:?}", service);
        let c = fs::read_to_string(&service).unwrap();
        assert!(c.contains("OrderService"));
    }

    #[test]
    fn test_gen_api_creates_controller() {
        let guard = WithTempDir::new();
        let tmp = guard.path();

        kegani_cli::commands::init::init("ctrl-api-test", "api").expect("init");
        std::env::set_current_dir(&tmp.join("ctrl-api-test")).expect("change dir");

        kegani_cli::commands::gen::gen(GenKind::Api, "item").expect("gen api");

        let ctrl = tmp.join("ctrl-api-test/src/controller/api/item.rs");
        assert!(ctrl.exists(), "controller file should exist: {:?}", ctrl);
        let c = fs::read_to_string(&ctrl).unwrap();
        assert!(c.contains("pub async fn list"));
        assert!(c.contains("pub async fn get"));
        assert!(c.contains("pub async fn create"));
        assert!(c.contains("pub async fn delete"));
        assert!(c.contains("pub async fn update"));
    }

    #[test]
    fn test_gen_api_creates_routes() {
        let guard = WithTempDir::new();
        let tmp = guard.path();

        kegani_cli::commands::init::init("routes-api-test", "api").expect("init");
        std::env::set_current_dir(&tmp.join("routes-api-test")).expect("change dir");

        kegani_cli::commands::gen::gen(GenKind::Api, "comment").expect("gen api");

        let routes = tmp.join("routes-api-test/src/routes/api/v1/comment.rs");
        assert!(routes.exists(), "routes file should exist: {:?}", routes);
        let c = fs::read_to_string(&routes).unwrap();
        assert!(c.contains("web::scope(\"/comment\")"));
        assert!(c.contains("web::get"));
        assert!(c.contains("web::post"));
    }
}

// ---------------------------------------------------------------------------
// `gen model` tests
// ---------------------------------------------------------------------------

mod gen_model_tests {
    use super::*;

    #[test]
    fn test_gen_model_creates_entity() {
        let guard = WithTempDir::new();
        let tmp = guard.path();

        kegani_cli::commands::init::init("model-test", "api").expect("init");
        std::env::set_current_dir(&tmp.join("model-test")).expect("change dir");

        kegani_cli::commands::gen::gen(GenKind::Model, "book").expect("gen model");

        let entity = tmp.join("model-test/internal/model/entity/book.rs");
        assert!(entity.exists(), "entity file should exist");
        let c = fs::read_to_string(&entity).unwrap();
        assert!(c.contains("pub struct Book"));
        assert!(c.contains("pub id: Uuid"));
        assert!(c.contains("pub name: String"));
    }
}

// ---------------------------------------------------------------------------
// `gen migration` tests
// ---------------------------------------------------------------------------

mod gen_migration_tests {
    use super::*;

    #[test]
    fn test_gen_migration_creates_sql_file() {
        let guard = WithTempDir::new();
        let tmp = guard.path();

        kegani_cli::commands::init::init("mig-test", "api").expect("init");
        std::env::set_current_dir(&tmp.join("mig-test")).expect("change dir");

        kegani_cli::commands::gen::gen(GenKind::Migration, "create_posts").expect("gen migration");

        let migrations_dir = tmp.join("mig-test/migrations");
        let mut entries: Vec<_> = fs::read_dir(migrations_dir)
            .unwrap()
            .filter_map(|e| e.ok())
            .collect();
        entries.sort_by_key(|e| e.path());

        let last = entries.last().expect("a migration file should exist");
        let c = fs::read_to_string(last.path()).unwrap();
        assert!(c.contains("create_posts"), "should mention migration name");
        assert!(c.contains("CREATE TABLE"), "should have CREATE TABLE");
        assert!(c.contains("id UUID PRIMARY KEY"), "should have id PK");
        assert!(c.contains("name VARCHAR(255)"), "should have name column");
    }

    #[test]
    fn test_gen_migration_has_timestamp_prefix() {
        let guard = WithTempDir::new();
        let tmp = guard.path();

        kegani_cli::commands::init::init("ts-test", "api").expect("init");
        std::env::set_current_dir(&tmp.join("ts-test")).expect("change dir");

        kegani_cli::commands::gen::gen(GenKind::Migration, "books").expect("gen migration");

        let migrations_dir = tmp.join("ts-test/migrations");
        let mut entries: Vec<_> = fs::read_dir(migrations_dir).unwrap().filter_map(|e| e.ok()).collect();
        entries.sort_by_key(|e| e.path());
        let binding = entries.last().unwrap().file_name();
        let name = binding.to_str().unwrap();

        assert!(
            name.starts_with(|c: char| c.is_ascii_digit()),
            "migration filename should start with digits (timestamp): got {:?}",
            name
        );
        assert!(name.ends_with("_books.sql"), "filename should end with _books.sql");
    }
}

// ---------------------------------------------------------------------------
// `gen controller` tests
// ---------------------------------------------------------------------------

mod gen_controller_tests {
    use super::*;

    #[test]
    fn test_gen_controller_creates_controller_and_routes() {
        let guard = WithTempDir::new();
        let tmp = guard.path();

        kegani_cli::commands::init::init("ctrl-gen-test", "api").expect("init");
        std::env::set_current_dir(&tmp.join("ctrl-gen-test")).expect("change dir");

        kegani_cli::commands::gen::gen(GenKind::Controller, "product").expect("gen controller");

        let ctrl = tmp.join("ctrl-gen-test/src/controller/api/product.rs");
        assert!(ctrl.exists(), "controller file should exist");
        let c = fs::read_to_string(&ctrl).unwrap();
        assert!(c.contains("pub async fn list"));
        assert!(c.contains("pub async fn get"));
        assert!(c.contains("pub async fn create"));
        assert!(c.contains("pub async fn update"));
        assert!(c.contains("pub async fn delete"));

        let routes = tmp.join("ctrl-gen-test/src/routes/api/v1/product.rs");
        assert!(routes.exists(), "routes file should exist");
    }
}

// ---------------------------------------------------------------------------
// `gen service` tests
// ---------------------------------------------------------------------------

mod gen_service_tests {
    use super::*;

    #[test]
    fn test_gen_service_creates_service_file() {
        let guard = WithTempDir::new();
        let tmp = guard.path();

        kegani_cli::commands::init::init("svc-gen-test", "api").expect("init");
        std::env::set_current_dir(&tmp.join("svc-gen-test")).expect("change dir");

        kegani_cli::commands::gen::gen(GenKind::Service, "inventory").expect("gen service");

        let svc = tmp.join("svc-gen-test/internal/service/inventory_service.rs");
        assert!(svc.exists(), "service file should exist");
        let c = fs::read_to_string(&svc).unwrap();
        assert!(c.contains("InventoryService"));
    }
}

// ---------------------------------------------------------------------------
// `gen repository` tests
// ---------------------------------------------------------------------------

mod gen_repository_tests {
    use super::*;

    #[test]
    fn test_gen_repository_creates_repo_file() {
        let guard = WithTempDir::new();
        let tmp = guard.path();

        kegani_cli::commands::init::init("repo-gen-test", "api").expect("init");
        std::env::set_current_dir(&tmp.join("repo-gen-test")).expect("change dir");

        kegani_cli::commands::gen::gen(GenKind::Repository, "category").expect("gen repository");

        let repo = tmp.join("repo-gen-test/internal/repository/category_repo.rs");
        assert!(repo.exists(), "repository file should exist");
        let c = fs::read_to_string(&repo).unwrap();
        assert!(c.contains("CategoryRepository"));
        assert!(c.contains("PgPool"));
    }
}

// ---------------------------------------------------------------------------
// `gen middleware` tests
// ---------------------------------------------------------------------------

mod gen_middleware_tests {
    use super::*;

    #[test]
    fn test_gen_middleware_creates_middleware_file() {
        let guard = WithTempDir::new();
        let tmp = guard.path();

        kegani_cli::commands::init::init("mw-gen-test", "api").expect("init");
        std::env::set_current_dir(&tmp.join("mw-gen-test")).expect("change dir");

        kegani_cli::commands::gen::gen(GenKind::Middleware, "ratelimit").expect("gen middleware");

        let mw = tmp.join("mw-gen-test/src/middleware/ratelimit.rs");
        assert!(mw.exists(), "middleware file should exist");
    }
}