use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

use cucumber::{given, then, when};
use serde_json::Value;

use kanbus::file_io::load_project_directory;
use kanbus::migration::migrate_from_beads;

use crate::step_definitions::initialization_steps::KanbusWorld;

fn fixture_beads_dir() -> PathBuf {
    let root = Path::new(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .expect("repo root");
    root.join("specs")
        .join("fixtures")
        .join("beads_repo")
        .join(".beads")
}

fn copy_dir(source: &Path, destination: &Path) {
    fs::create_dir_all(destination).expect("create destination");
    for entry in fs::read_dir(source).expect("read source") {
        let entry = entry.expect("entry");
        let path = entry.path();
        let target = destination.join(entry.file_name());
        if path.is_dir() {
            copy_dir(&path, &target);
        } else {
            fs::copy(&path, &target).expect("copy file");
        }
    }
}

fn load_project_dir(world: &KanbusWorld) -> PathBuf {
    let cwd = world.working_directory.as_ref().expect("cwd");
    load_project_directory(cwd).expect("project dir")
}

#[given("a git repository with a .beads issues database")]
fn given_repo_with_beads(world: &mut KanbusWorld) {
    let temp_dir = tempfile::TempDir::new().expect("tempdir");
    let repo_path = temp_dir.path().join("repo");
    fs::create_dir_all(&repo_path).expect("create repo dir");
    Command::new("git")
        .args(["init"])
        .current_dir(&repo_path)
        .output()
        .expect("git init failed");
    let target_beads = repo_path.join(".beads");
    copy_dir(&fixture_beads_dir(), &target_beads);
    world.working_directory = Some(repo_path);
    world.temp_dir = Some(temp_dir);
}

#[given("a git repository with a .beads issues database containing blank lines")]
fn given_repo_with_blank_lines(world: &mut KanbusWorld) {
    let temp_dir = tempfile::TempDir::new().expect("tempdir");
    let repo_path = temp_dir.path().join("repo");
    fs::create_dir_all(&repo_path).expect("create repo dir");
    Command::new("git")
        .args(["init"])
        .current_dir(&repo_path)
        .output()
        .expect("git init failed");
    let beads_dir = repo_path.join(".beads");
    fs::create_dir_all(&beads_dir).expect("beads dir");
    let record = serde_json::json!({
        "id": "kanbus-001",
        "title": "Title",
        "issue_type": "task",
        "status": "open",
        "priority": 2,
        "created_at": "2026-02-11T00:00:00Z",
        "updated_at": "2026-02-11T00:00:00Z",
        "dependencies": [],
        "comments": []
    });
    let lines = format!(
        "{}\n\n{}\n",
        record.to_string(),
        serde_json::json!({
            "id": "kanbus-002",
            "title": "Title",
            "issue_type": "task",
            "status": "open",
            "priority": 2,
            "created_at": "2026-02-11T00:00:00Z",
            "updated_at": "2026-02-11T00:00:00Z",
            "dependencies": [],
            "comments": []
        })
        .to_string()
    );
    fs::write(beads_dir.join("issues.jsonl"), lines).expect("write issues");
    world.working_directory = Some(repo_path);
    world.temp_dir = Some(temp_dir);
}

#[given("a git repository with Beads metadata and dependencies")]
fn given_repo_with_metadata(world: &mut KanbusWorld) {
    let temp_dir = tempfile::TempDir::new().expect("tempdir");
    let repo_path = temp_dir.path().join("repo");
    fs::create_dir_all(&repo_path).expect("create repo dir");
    Command::new("git")
        .args(["init"])
        .current_dir(&repo_path)
        .output()
        .expect("git init failed");
    let beads_dir = repo_path.join(".beads");
    fs::create_dir_all(&beads_dir).expect("beads dir");
    let base = serde_json::json!({
        "title": "Title",
        "issue_type": "task",
        "status": "open",
        "priority": 2,
        "created_at": "2026-02-11T00:00:00Z",
        "updated_at": "2026-02-11T00:00:00Z",
        "dependencies": [],
        "comments": []
    });
    let parent = serde_json::json!({
        "id": "kanbus-parent",
        "title": base["title"],
        "issue_type": base["issue_type"],
        "status": base["status"],
        "priority": base["priority"],
        "created_at": base["created_at"],
        "updated_at": base["updated_at"],
        "dependencies": base["dependencies"],
        "comments": base["comments"]
    });
    let child = serde_json::json!({
        "id": "kanbus-child",
        "title": base["title"],
        "issue_type": base["issue_type"],
        "status": base["status"],
        "priority": base["priority"],
        "created_at": base["created_at"],
        "updated_at": base["updated_at"],
        "dependencies": [{"type": "blocked-by", "depends_on_id": "kanbus-parent"}],
        "comments": base["comments"],
        "notes": "Notes",
        "acceptance_criteria": "Criteria",
        "close_reason": "Done",
        "owner": "dev@example.com"
    });
    let lines = format!("{}\n{}", parent.to_string(), child.to_string());
    fs::write(beads_dir.join("issues.jsonl"), lines).expect("write issues");
    world.working_directory = Some(repo_path);
    world.temp_dir = Some(temp_dir);
}

#[given("a git repository with a Beads feature issue")]
fn given_repo_with_feature_issue(world: &mut KanbusWorld) {
    let temp_dir = tempfile::TempDir::new().expect("tempdir");
    let repo_path = temp_dir.path().join("repo");
    fs::create_dir_all(&repo_path).expect("create repo dir");
    Command::new("git")
        .args(["init"])
        .current_dir(&repo_path)
        .output()
        .expect("git init failed");
    let beads_dir = repo_path.join(".beads");
    fs::create_dir_all(&beads_dir).expect("beads dir");
    let record = serde_json::json!({
        "id": "bdx-feature",
        "title": "Feature issue",
        "issue_type": "feature",
        "status": "open",
        "priority": 2,
        "created_at": "2026-02-11T00:00:00Z",
        "updated_at": "2026-02-11T00:00:00Z",
        "dependencies": [],
        "comments": []
    });
    fs::write(beads_dir.join("issues.jsonl"), record.to_string()).expect("write issues");
    world.working_directory = Some(repo_path);
    world.temp_dir = Some(temp_dir);
}

#[given("a git repository with Beads epic parent and child")]
fn given_repo_with_epic_parent_child(world: &mut KanbusWorld) {
    let temp_dir = tempfile::TempDir::new().expect("tempdir");
    let repo_path = temp_dir.path().join("repo");
    fs::create_dir_all(&repo_path).expect("create repo dir");
    Command::new("git")
        .args(["init"])
        .current_dir(&repo_path)
        .output()
        .expect("git init failed");
    let beads_dir = repo_path.join(".beads");
    fs::create_dir_all(&beads_dir).expect("beads dir");
    let parent = serde_json::json!({
        "id": "bdx-parent",
        "title": "Parent epic",
        "issue_type": "epic",
        "status": "open",
        "priority": 2,
        "created_at": "2026-02-11T00:00:00Z",
        "updated_at": "2026-02-11T00:00:00Z",
        "dependencies": [],
        "comments": []
    });
    let child = serde_json::json!({
        "id": "bdx-child",
        "title": "Child epic",
        "issue_type": "epic",
        "status": "open",
        "priority": 2,
        "created_at": "2026-02-11T00:00:00Z",
        "updated_at": "2026-02-11T00:00:00Z",
        "dependencies": [
            {
                "issue_id": "bdx-child",
                "depends_on_id": "bdx-parent",
                "type": "parent-child",
                "created_at": "2026-02-11T00:00:00Z",
                "created_by": "dev@example.com"
            }
        ],
        "comments": []
    });
    let lines = format!("{}\n{}", parent.to_string(), child.to_string());
    fs::write(beads_dir.join("issues.jsonl"), lines).expect("write issues");
    world.working_directory = Some(repo_path);
    world.temp_dir = Some(temp_dir);
}

#[given("a git repository with Beads issues containing fractional timestamps")]
fn given_repo_with_fractional_timestamps(world: &mut KanbusWorld) {
    let temp_dir = tempfile::TempDir::new().expect("tempdir");
    let repo_path = temp_dir.path().join("repo");
    fs::create_dir_all(&repo_path).expect("create repo dir");
    Command::new("git")
        .args(["init"])
        .current_dir(&repo_path)
        .output()
        .expect("git init failed");
    let beads_dir = repo_path.join(".beads");
    fs::create_dir_all(&beads_dir).expect("beads dir");
    let records = vec![
        serde_json::json!({
            "id": "bdx-frac-short",
            "title": "Short fractional",
            "issue_type": "task",
            "status": "open",
            "priority": 2,
            "created_at": "2026-02-11T00:00:00.1+00:00",
            "updated_at": "2026-02-11T00:00:00.1+00:00",
            "dependencies": [],
            "comments": []
        }),
        serde_json::json!({
            "id": "bdx-frac-long",
            "title": "Long fractional",
            "issue_type": "task",
            "status": "open",
            "priority": 2,
            "created_at": "2026-02-11T00:00:00.1234567+00:00",
            "updated_at": "2026-02-11T00:00:00.1234567+00:00",
            "dependencies": [],
            "comments": []
        }),
        serde_json::json!({
            "id": "bdx-frac-nozone",
            "title": "No zone",
            "issue_type": "task",
            "status": "open",
            "priority": 2,
            "created_at": "2026-02-11T00:00:00.123",
            "updated_at": "2026-02-11T00:00:00.123",
            "dependencies": [],
            "comments": []
        }),
        serde_json::json!({
            "id": "bdx-frac-negative",
            "title": "Negative offset",
            "issue_type": "task",
            "status": "open",
            "priority": 2,
            "created_at": "2026-02-11T00:00:00.123456-05:00",
            "updated_at": "2026-02-11T00:00:00.123456-05:00",
            "dependencies": [],
            "comments": []
        }),
    ];
    let lines = records
        .into_iter()
        .map(|record| record.to_string())
        .collect::<Vec<_>>()
        .join("\n");
    fs::write(beads_dir.join("issues.jsonl"), lines).expect("write issues");
    world.working_directory = Some(repo_path);
    world.temp_dir = Some(temp_dir);
}

#[given("a Kanbus project already exists")]
fn given_kanbus_project_exists(world: &mut KanbusWorld) {
    let cwd = world.working_directory.as_ref().expect("cwd");
    fs::create_dir_all(cwd.join("project").join("issues")).expect("create project");
}

#[given("a git repository without a .beads directory")]
fn given_repo_without_beads(world: &mut KanbusWorld) {
    let temp_dir = tempfile::TempDir::new().expect("tempdir");
    let repo_path = temp_dir.path().join("repo");
    fs::create_dir_all(&repo_path).expect("create repo dir");
    Command::new("git")
        .args(["init"])
        .current_dir(&repo_path)
        .output()
        .expect("git init failed");
    world.working_directory = Some(repo_path);
    world.temp_dir = Some(temp_dir);
}

#[given("a git repository with an empty .beads directory")]
fn given_repo_empty_beads(world: &mut KanbusWorld) {
    let temp_dir = tempfile::TempDir::new().expect("tempdir");
    let repo_path = temp_dir.path().join("repo");
    fs::create_dir_all(&repo_path).expect("create repo dir");
    Command::new("git")
        .args(["init"])
        .current_dir(&repo_path)
        .output()
        .expect("git init failed");
    fs::create_dir_all(repo_path.join(".beads")).expect("create beads dir");
    world.working_directory = Some(repo_path);
    world.temp_dir = Some(temp_dir);
}

#[given("a git repository with an empty issues.jsonl file")]
fn given_repo_with_empty_issues_jsonl(world: &mut KanbusWorld) {
    let temp_dir = tempfile::TempDir::new().expect("tempdir");
    let repo_path = temp_dir.path().join("repo");
    fs::create_dir_all(&repo_path).expect("create repo dir");
    Command::new("git")
        .args(["init"])
        .current_dir(&repo_path)
        .output()
        .expect("git init failed");
    let beads_dir = repo_path.join(".beads");
    fs::create_dir_all(&beads_dir).expect("create beads dir");
    fs::write(beads_dir.join("issues.jsonl"), "").expect("write empty issues");
    world.working_directory = Some(repo_path);
    world.temp_dir = Some(temp_dir);
}

#[given("a git repository with a .beads issues database containing an invalid id")]
fn given_repo_with_invalid_beads_id(world: &mut KanbusWorld) {
    let temp_dir = tempfile::TempDir::new().expect("tempdir");
    let repo_path = temp_dir.path().join("repo");
    fs::create_dir_all(&repo_path).expect("create repo dir");
    Command::new("git")
        .args(["init"])
        .current_dir(&repo_path)
        .output()
        .expect("git init failed");
    let beads_dir = repo_path.join(".beads");
    fs::create_dir_all(&beads_dir).expect("create beads dir");
    let record = serde_json::json!({
        "id": "invalidid",
        "title": "Title",
        "issue_type": "task",
        "status": "open",
        "priority": 2,
        "created_at": "2026-02-11T00:00:00Z",
        "updated_at": "2026-02-11T00:00:00Z",
        "dependencies": [],
        "comments": []
    });
    fs::write(beads_dir.join("issues.jsonl"), record.to_string()).expect("write issues");
    world.working_directory = Some(repo_path);
    world.temp_dir = Some(temp_dir);
}

#[when("I validate migration error cases")]
fn when_validate_migration_errors(world: &mut KanbusWorld) {
    let mut errors = Vec::new();

    let mut run_case = |records: Vec<serde_json::Value>, label: &str| {
        let temp_dir = tempfile::TempDir::new().expect("tempdir");
        let repo_path = temp_dir.path().join(format!("case-{label}"));
        fs::create_dir_all(&repo_path).expect("create repo dir");
        Command::new("git")
            .args(["init"])
            .current_dir(&repo_path)
            .output()
            .expect("git init failed");
        let beads_dir = repo_path.join(".beads");
        fs::create_dir_all(&beads_dir).expect("create beads dir");
        let lines: Vec<String> = records
            .into_iter()
            .map(|record| serde_json::to_string(&record).expect("serialize record"))
            .collect();
        fs::write(beads_dir.join("issues.jsonl"), lines.join("\n")).expect("write issues");
        match migrate_from_beads(&repo_path) {
            Ok(_) => errors.push("expected error not raised".to_string()),
            Err(error) => errors.push(error.to_string()),
        }
    };

    let valid_base = serde_json::json!({
        "id": "kanbus-001",
        "title": "Title",
        "issue_type": "task",
        "status": "open",
        "priority": 2,
        "closed_at": serde_json::Value::Null,
        "created_at": "2026-02-11T00:00:00Z",
        "updated_at": "2026-02-11T00:00:00Z",
        "dependencies": [],
        "comments": []
    });

    let build_record = |base: &serde_json::Value, updates: Vec<(&str, serde_json::Value)>| {
        let mut map = base.as_object().expect("object").clone();
        for (key, value) in updates {
            map.insert(key.to_string(), value);
        }
        serde_json::Value::Object(map)
    };

    run_case(
        vec![serde_json::json!({"title": "Missing id"})],
        "missing-id",
    );
    run_case(
        vec![build_record(
            &valid_base,
            vec![("title", serde_json::json!(""))],
        )],
        "missing-title",
    );
    run_case(
        vec![build_record(
            &valid_base,
            vec![("issue_type", serde_json::json!(""))],
        )],
        "missing-type",
    );
    run_case(
        vec![build_record(
            &valid_base,
            vec![("status", serde_json::json!(""))],
        )],
        "missing-status",
    );
    let mut record_without_priority = valid_base.as_object().expect("object").clone();
    record_without_priority.remove("priority");
    run_case(
        vec![serde_json::Value::Object(record_without_priority)],
        "missing-priority-field",
    );
    run_case(
        vec![build_record(
            &valid_base,
            vec![("priority", serde_json::Value::Null)],
        )],
        "missing-priority",
    );
    run_case(
        vec![build_record(
            &valid_base,
            vec![("priority", serde_json::json!(99))],
        )],
        "invalid-priority",
    );
    run_case(
        vec![build_record(
            &valid_base,
            vec![("issue_type", serde_json::json!("unknown"))],
        )],
        "unknown-type",
    );
    run_case(
        vec![build_record(
            &valid_base,
            vec![("status", serde_json::json!("invalid"))],
        )],
        "invalid-status",
    );
    run_case(
        vec![build_record(
            &valid_base,
            vec![(
                "dependencies",
                serde_json::json!([{"type": "", "depends_on_id": ""}]),
            )],
        )],
        "invalid-dependency",
    );
    run_case(
        vec![build_record(
            &valid_base,
            vec![(
                "dependencies",
                serde_json::json!([{"type": "blocked-by", "depends_on_id": "kanbus-missing"}]),
            )],
        )],
        "missing-dependency",
    );
    run_case(
        vec![
            build_record(
                &valid_base,
                vec![
                    ("id", serde_json::json!("kanbus-child")),
                    (
                        "dependencies",
                        serde_json::json!([
                            {"type": "parent-child", "depends_on_id": "kanbus-parent"},
                            {"type": "parent-child", "depends_on_id": "kanbus-parent-2"}
                        ]),
                    ),
                ],
            ),
            build_record(
                &valid_base,
                vec![("id", serde_json::json!("kanbus-parent"))],
            ),
            build_record(
                &valid_base,
                vec![("id", serde_json::json!("kanbus-parent-2"))],
            ),
        ],
        "multiple-parents",
    );
    run_case(
        vec![
            serde_json::json!({
                "id": "kanbus-child",
                "title": "Child",
                "issue_type": "task",
                "status": "open",
                "priority": 2,
                "created_at": "2026-02-11T00:00:00Z",
                "updated_at": "2026-02-11T00:00:00Z",
                "dependencies": [{"type": "parent-child", "depends_on_id": "kanbus-parent"}],
                "comments": []
            }),
            serde_json::json!({
                "id": "kanbus-parent",
                "title": "Parent",
                "status": "open",
                "priority": 2,
                "created_at": "2026-02-11T00:00:00Z",
                "updated_at": "2026-02-11T00:00:00Z"
            }),
        ],
        "parent-issue-type-missing",
    );
    run_case(
        vec![build_record(
            &valid_base,
            vec![(
                "comments",
                serde_json::json!([{"author": "", "text": "bad", "created_at": "2026-02-11T00:00:00Z"}]),
            )],
        )],
        "invalid-comment",
    );
    run_case(
        vec![build_record(
            &valid_base,
            vec![(
                "comments",
                serde_json::json!([{"author": "dev", "text": "ok"}]),
            )],
        )],
        "comment-created-missing",
    );
    run_case(
        vec![build_record(
            &valid_base,
            vec![(
                "comments",
                serde_json::json!([{"author": "dev", "text": "ok", "created_at": 123}]),
            )],
        )],
        "comment-created-not-string",
    );
    run_case(
        vec![build_record(
            &valid_base,
            vec![(
                "comments",
                serde_json::json!([{"author": "dev", "text": "ok", "created_at": "bad"}]),
            )],
        )],
        "comment-created-invalid",
    );
    run_case(
        vec![build_record(
            &valid_base,
            vec![(
                "comments",
                serde_json::json!([{"author": "dev", "text": "ok", "created_at": ""}]),
            )],
        )],
        "comment-created-empty",
    );
    run_case(
        vec![build_record(
            &valid_base,
            vec![("created_at", serde_json::Value::Null)],
        )],
        "created-missing",
    );
    run_case(
        vec![build_record(
            &valid_base,
            vec![("created_at", serde_json::json!(""))],
        )],
        "created-empty",
    );
    run_case(
        vec![build_record(
            &valid_base,
            vec![("created_at", serde_json::json!(123))],
        )],
        "created-not-string",
    );
    run_case(
        vec![build_record(
            &valid_base,
            vec![("created_at", serde_json::json!("invalid"))],
        )],
        "created-invalid",
    );
    run_case(
        vec![build_record(
            &valid_base,
            vec![(
                "created_at",
                serde_json::json!("2026-02-11T00:00:00.bad+00:00"),
            )],
        )],
        "created-invalid-fractional",
    );
    run_case(
        vec![build_record(
            &valid_base,
            vec![(
                "created_at",
                serde_json::json!("2026-02-11T00:00:00.bad-00:00"),
            )],
        )],
        "created-invalid-negative",
    );
    run_case(
        vec![build_record(
            &valid_base,
            vec![(
                "created_at",
                serde_json::json!("2026-02-11T00:00:00.123+00:00-00"),
            )],
        )],
        "created-invalid-mixed-offset",
    );

    world.migration_errors = errors;
}

#[then("a Kanbus project should be initialized")]
fn then_kanbus_initialized(world: &mut KanbusWorld) {
    let project_dir = load_project_dir(world);
    assert!(project_dir.is_dir());
}

#[then("all Beads issues should be converted to Kanbus issues")]
fn then_beads_converted(world: &mut KanbusWorld) {
    let cwd = world.working_directory.as_ref().expect("cwd");
    let issues_path = cwd.join(".beads").join("issues.jsonl");
    let contents = fs::read_to_string(issues_path).expect("read issues");
    let line_count = contents
        .lines()
        .filter(|line| !line.trim().is_empty())
        .count();
    let project_dir = load_project_dir(world);
    let issue_files = fs::read_dir(project_dir.join("issues"))
        .expect("read issues dir")
        .filter_map(|entry| entry.ok())
        .filter(|entry| entry.path().extension().and_then(|ext| ext.to_str()) == Some("json"))
        .count();
    assert_eq!(issue_files, line_count);
}

#[then("migrated issues should include metadata and dependencies")]
fn then_migration_includes_metadata(world: &mut KanbusWorld) {
    let project_dir = load_project_dir(world);
    let issue_path = project_dir.join("issues").join("kanbus-child.json");
    let contents = fs::read_to_string(issue_path).expect("read issue");
    let payload: serde_json::Value = serde_json::from_str(&contents).expect("parse issue");
    let custom = payload
        .get("custom")
        .and_then(|value| value.as_object())
        .expect("custom");
    assert_eq!(
        custom.get("beads_notes").and_then(|v| v.as_str()),
        Some("Notes")
    );
    assert_eq!(
        custom
            .get("beads_acceptance_criteria")
            .and_then(|v| v.as_str()),
        Some("Criteria")
    );
    assert_eq!(
        custom.get("beads_close_reason").and_then(|v| v.as_str()),
        Some("Done")
    );
    assert_eq!(
        custom.get("beads_owner").and_then(|v| v.as_str()),
        Some("dev@example.com")
    );
    let deps = payload
        .get("dependencies")
        .and_then(|v| v.as_array())
        .expect("deps");
    let has_dep = deps.iter().any(|item| {
        item.get("target").and_then(|v| v.as_str()) == Some("kanbus-parent")
            && item.get("type").and_then(|v| v.as_str()) == Some("blocked-by")
    });
    assert!(has_dep);
}

#[then(expr = "migrated issue {string} should have type {string}")]
fn then_migrated_issue_type(world: &mut KanbusWorld, identifier: String, issue_type: String) {
    let project_dir = load_project_dir(world);
    let issue_path = project_dir
        .join("issues")
        .join(format!("{identifier}.json"));
    let contents = fs::read_to_string(issue_path).expect("read issue");
    let payload: Value = serde_json::from_str(&contents).expect("parse issue");
    assert_eq!(
        payload.get("type").and_then(|value| value.as_str()),
        Some(issue_type.as_str())
    );
}

#[then(expr = "migrated issue {string} should have parent {string}")]
fn then_migrated_issue_parent(world: &mut KanbusWorld, identifier: String, parent: String) {
    let project_dir = load_project_dir(world);
    let issue_path = project_dir
        .join("issues")
        .join(format!("{identifier}.json"));
    let contents = fs::read_to_string(issue_path).expect("read issue");
    let payload: Value = serde_json::from_str(&contents).expect("parse issue");
    assert_eq!(
        payload.get("parent").and_then(|value| value.as_str()),
        Some(parent.as_str())
    );
}

#[then(expr = "migrated issue {string} should preserve beads issue type {string}")]
fn then_migrated_issue_preserves_type(
    world: &mut KanbusWorld,
    identifier: String,
    issue_type: String,
) {
    let project_dir = load_project_dir(world);
    let issue_path = project_dir
        .join("issues")
        .join(format!("{identifier}.json"));
    let contents = fs::read_to_string(issue_path).expect("read issue");
    let payload: Value = serde_json::from_str(&contents).expect("parse issue");
    let custom = payload.get("custom").and_then(|value| value.as_object());
    assert_eq!(
        custom
            .and_then(|map| map.get("beads_issue_type"))
            .and_then(|value| value.as_str()),
        Some(issue_type.as_str())
    );
}

#[then(expr = "migration errors should include {string}")]
fn then_migration_errors_include(world: &mut KanbusWorld, message: String) {
    assert!(world.migration_errors.contains(&message));
}
