use assert_cmd::Command;
use chrono::{Duration, Utc};
use predicates::str::contains;
use predicates::str::is_match;
use serial_test::serial;

mod util;
use util::setup_temp_git_repo;

/// Tests that the status command outputs the expected status message.
#[test]
#[serial]
fn test_status_command() {
    // Add the setup function to create a git repo for the test
    let (_dir, _bare_dir, repo_path) = setup_temp_git_repo();
    std::env::set_current_dir(&repo_path).unwrap();

    let mut cmd = Command::cargo_bin("tbdflow").unwrap();
    cmd.arg("--verbose").arg("status");
    cmd.assert().success().stdout(contains("Checking status"));
}

/// Tests that the current branch command outputs the expected branch name.
#[test]
#[serial]
fn test_current_branch_command() {
    let (_dir, _bare_dir, repo_path) = setup_temp_git_repo();
    std::env::set_current_dir(&repo_path).unwrap();
    let mut cmd = Command::cargo_bin("tbdflow").unwrap();
    cmd.arg("current-branch");
    cmd.assert()
        .success()
        .stdout(contains("Current branch is:"));
}

#[test]
#[serial]
fn test_branch_command() {
    // 'branch-name' strategy
    let (_dir, _bare_dir, repo_path) = setup_temp_git_repo();
    std::env::set_current_dir(&repo_path).unwrap();

    // Create a minimal but complete .tbdflow.yml file
    let config_content = r#"
main_branch_name: main
stale_branch_threshold_days: 1
issue_handling:
  strategy: "branch-name"
branch_types:
  feat: "feat/"
  fix: "fix/"
branch_prefixes:
  feature: "feature_"
  release: "release_"
  hotfix: "hotfix_"
automatic_tags:
  release_prefix: "v"
  hotfix_prefix: "hotfix-tag_"
"#;
    std::fs::write(repo_path.join(".tbdflow.yml"), config_content).unwrap();

    // Commit the config file to clean the working directory
    std::process::Command::new("git")
        .args(&["add", ".tbdflow.yml"])
        .current_dir(&repo_path)
        .output()
        .unwrap();
    std::process::Command::new("git")
        .args(&["commit", "-m", "Add tbdflow config"])
        .current_dir(&repo_path)
        .output()
        .unwrap();

    // Test creating a branch WITH an issue ID
    let mut cmd_with_issue = Command::cargo_bin("tbdflow").unwrap();
    cmd_with_issue
        .arg("branch")
        .arg("--type")
        .arg("feat")
        .arg("--name")
        .arg("new-dashboard")
        .arg("--issue")
        .arg("PROJ-123");

    cmd_with_issue.assert().success().stdout(contains(
        "Success! Switched to new branch: 'feat/PROJ-123-new-dashboard'",
    ));

    // 'commit-scope' strategy
    let (_dir2, _bare_dir2, repo_path2) = setup_temp_git_repo();
    std::env::set_current_dir(&repo_path2).unwrap();

    // Create a minimal but complete .tbdflow.yml file
    let config_content_2 = r#"
main_branch_name: main
stale_branch_threshold_days: 1
issue_handling:
  strategy: "commit-scope"
branch_types:
  feat: "feat/"
  fix: "fix/"
branch_prefixes:
  feature: "feature_"
  release: "release_"
  hotfix: "hotfix_"
automatic_tags:
  release_prefix: "v"
  hotfix_prefix: "hotfix-tag_"
"#;
    std::fs::write(repo_path2.join(".tbdflow.yml"), config_content_2).unwrap();

    // Commit the config file to clean the working directory
    std::process::Command::new("git")
        .args(&["add", ".tbdflow.yml"])
        .current_dir(&repo_path2)
        .output()
        .unwrap();
    std::process::Command::new("git")
        .args(&["commit", "-m", "Add tbdflow config"])
        .current_dir(&repo_path2)
        .output()
        .unwrap();

    // Test creating a branch WITHOUT an issue ID in the name
    let mut cmd_without_issue = Command::cargo_bin("tbdflow").unwrap();
    cmd_without_issue
        .arg("branch")
        .arg("--type")
        .arg("fix")
        .arg("--name")
        .arg("login-bug")
        .arg("--issue")
        .arg("PROJ-456"); // This should be ignored for the branch name

    cmd_without_issue
        .assert()
        .success()
        .stdout(contains("Success! Switched to new branch: 'fix/login-bug'"));
}

/// Tests that creating a new feature branch called "new-feature" works correctly.
#[test]
#[serial]
fn test_create_feature_branch_command() {
    let (_dir, _bare_dir, repo_path) = setup_temp_git_repo();
    std::env::set_current_dir(&repo_path).unwrap();
    let mut cmd = Command::cargo_bin("tbdflow").unwrap();
    cmd.arg("branch")
        .arg("--type")
        .arg("feature")
        .arg("--name")
        .arg("new-feature");
    cmd.assert().success().stdout(contains(
        "Success! Switched to new branch: 'feature_new-feature'",
    ));
}

/// Tests that creating a new release branch called "1.0.0" works correctly.
#[test]
#[serial]
fn test_create_release_branch_command() {
    let (_dir, _bare_dir, repo_path) = setup_temp_git_repo();
    std::env::set_current_dir(&repo_path).unwrap();
    let mut cmd = Command::cargo_bin("tbdflow").unwrap();
    cmd.arg("branch")
        .arg("--type")
        .arg("release")
        .arg("--name")
        .arg("1.0.0");
    cmd.assert()
        .success()
        .stdout(contains("Success! Switched to new branch: 'release_1.0.0'"));
}

/// Tests that adding a new file and committing it with the commit command works correctly.
/// Skipping .dod.yml verification for simplicity in this test.
#[test]
#[serial]
fn test_commit_command() {
    let (_dir, _bare_dir, repo_path) = setup_temp_git_repo();
    std::env::set_current_dir(&repo_path).unwrap();

    // Create a file to commit
    std::fs::write(repo_path.join("BUTTON.md"), "this is a new button ■").unwrap();
    // Stage the file
    Command::new("git")
        .arg("add")
        .arg("BUTTON.md")
        .current_dir(&repo_path)
        .output()
        .unwrap();

    // Wait until the working directory is clean
    let mut retries = 5;
    while retries > 0 {
        let status = Command::new("git")
            .args(&["status", "--porcelain"])
            .current_dir(&repo_path)
            .output()
            .unwrap();
        if status.stdout.is_empty() {
            break;
        }
        std::thread::sleep(std::time::Duration::from_millis(100));
        retries -= 1;
    }

    let mut cmd = Command::cargo_bin("tbdflow").unwrap();
    // Run the commit command with a feature type, scope, message, and breaking change flag
    cmd.arg("commit")
        .arg("--type")
        .arg("feat")
        .arg("--scope")
        .arg("ui")
        .arg("--message")
        .arg("add new button")
        .arg("--body")
        .arg("This button is used for submitting forms.")
        .arg("--breaking")
        .arg("--tag")
        .arg("button-v1");
    cmd.assert().success().stdout(
        /*
        Depending on Git version used, output may vary slightly. For example:
        - Successfully committed and pushed changes to BRANCH_NAME.
        - Successfully pushed changes to 'BRANCH_NAME'.

        Also default branch name can be 'main' or 'master', depending on the Git version
        and distribution. Below is a regex that matches all those cases.
        */
        is_match(r"Successfully (?:committed and )?pushed changes to '?(?:main|master)'?\.")
            .unwrap(),
    );

    // Check that the tag exists
    let output = std::process::Command::new("git")
        .arg("tag")
        .current_dir(&repo_path)
        .output()
        .unwrap();
    let tags = String::from_utf8_lossy(&output.stdout);
    assert!(
        tags.contains("button-v1"),
        "Expected tag button-v1 not found. Tags: {}",
        tags
    );
}

/// Tests that completing a feature branch called "new-feature" works correctly.
#[test]
#[serial]
fn test_complete_feature_branch_command() {
    let (_dir, _bare_dir, repo_path) = setup_temp_git_repo();
    std::env::set_current_dir(&repo_path).unwrap();

    // Create the feature branch first
    let mut create_cmd = Command::cargo_bin("tbdflow").unwrap();
    create_cmd
        .arg("branch")
        .arg("--type")
        .arg("feature")
        .arg("--name")
        .arg("new-feature");
    create_cmd.assert().success();

    let mut cmd = Command::cargo_bin("tbdflow").unwrap();
    cmd.arg("complete")
        .arg("--type")
        .arg("feature")
        .arg("--name")
        .arg("new-feature");
    cmd.assert()
        .success()
        .stdout(contains("Branch to complete: feature_new-feature"));
}

/// Tests that completing a release branch called "1.0.0" works correctly.
#[test]
#[serial]
fn test_complete_release_branch_command() {
    let (_dir, _bare_dir, repo_path) = setup_temp_git_repo();
    std::env::set_current_dir(&repo_path).unwrap();

    // Create the release branch first
    let mut create_cmd = Command::cargo_bin("tbdflow").unwrap();
    create_cmd
        .arg("branch")
        .arg("--type")
        .arg("release")
        .arg("--name")
        .arg("1.0.0");
    create_cmd.assert().success();

    // Ensure the branch exists locally
    let output = std::process::Command::new("git")
        .args(&["branch", "--list"])
        .current_dir(&repo_path)
        .output()
        .unwrap();
    let branches = String::from_utf8_lossy(&output.stdout);
    assert!(
        branches.contains("release_1.0.0"),
        "Expected branch release_1.0.0 not found. Branches: {}",
        branches
    );

    let mut cmd = Command::cargo_bin("tbdflow").unwrap();
    cmd.arg("--verbose")
        .arg("complete")
        .arg("--type")
        .arg("release")
        .arg("--name")
        .arg("1.0.0");
    cmd.assert()
        .success()
        .stdout(contains("Branch to complete: release_1.0.0"));

    // Check that the tag exists
    let output = std::process::Command::new("git")
        .arg("tag")
        .current_dir(&repo_path)
        .output()
        .unwrap();
    let tags = String::from_utf8_lossy(&output.stdout);
    assert!(
        tags.contains("v1.0.0"),
        "Expected tag v1.0.0 not found. Tags: {}",
        tags
    );
}

/// Testing the synch command to ensure it pulls changes from the remote repository
/// We will simulate a remote change by pushing to a bare repository and then running the sync command.
#[test]
#[serial]
fn test_sync_command() {
    let (_dir, bare_dir, repo_path) = setup_temp_git_repo();

    // Simulate a team member pushing changes to the remote
    let remote_repo_url = bare_dir.path().to_str().unwrap();
    let second_clone_dir = tempfile::tempdir().unwrap();
    std::process::Command::new("git")
        .args(&["clone", remote_repo_url, "."])
        .current_dir(second_clone_dir.path())
        .output()
        .unwrap();

    std::fs::write(second_clone_dir.path().join("REMOTE.md"), "remote change").unwrap();

    std::process::Command::new("git")
        .args(&["config", "user.email", "teammate@example.com"])
        .current_dir(second_clone_dir.path())
        .output()
        .unwrap();
    std::process::Command::new("git")
        .args(&["config", "user.name", "Teammate"])
        .current_dir(second_clone_dir.path())
        .output()
        .unwrap();

    std::process::Command::new("git")
        .args(&["add", "."])
        .current_dir(second_clone_dir.path())
        .output()
        .unwrap();
    std::process::Command::new("git")
        .args(&["commit", "-m", "feat: add remote file"])
        .current_dir(second_clone_dir.path())
        .output()
        .unwrap();
    std::process::Command::new("git")
        .args(&["push"])
        .current_dir(second_clone_dir.path())
        .output()
        .unwrap();

    // Now run the sync command
    std::env::set_current_dir(&repo_path).unwrap();
    let mut cmd = Command::cargo_bin("tbdflow").unwrap();
    cmd.arg("sync");
    cmd.assert()
        .success()
        .stdout(contains("Syncing with remote"));
}

/// Tests that the 'undo' command reverts a specific commit by SHA on the trunk.
#[test]
#[serial]
fn test_undo_command() {
    let (_dir, _bare_dir, repo_path) = setup_temp_git_repo();
    std::env::set_current_dir(&repo_path).unwrap();

    // Create a file and commit it so we have something to revert
    std::fs::write(repo_path.join("BAD_CHANGE.md"), "this breaks the build").unwrap();
    std::process::Command::new("git")
        .args(&["add", "."])
        .current_dir(&repo_path)
        .output()
        .unwrap();
    std::process::Command::new("git")
        .args(&["commit", "-m", "feat: add bad change"])
        .current_dir(&repo_path)
        .output()
        .unwrap();
    std::process::Command::new("git")
        .args(&["push"])
        .current_dir(&repo_path)
        .output()
        .unwrap();

    // Grab the SHA of the commit we just made using tbdflow head-sha
    let sha_output = Command::cargo_bin("tbdflow")
        .unwrap()
        .arg("head-sha")
        .output()
        .unwrap();
    let sha = String::from_utf8_lossy(&sha_output.stdout)
        .trim()
        .to_string();

    // The file should exist before undo
    assert!(repo_path.join("BAD_CHANGE.md").exists());

    // Run the undo command
    let mut cmd = Command::cargo_bin("tbdflow").unwrap();
    cmd.arg("undo").arg(&sha);
    cmd.assert()
        .success()
        .stdout(contains("Undo: The Panic Button"))
        .stdout(contains(&format!("Commit '{}' has been reverted", &sha)));

    // The file should be gone after the revert
    assert!(
        !repo_path.join("BAD_CHANGE.md").exists(),
        "BAD_CHANGE.md should have been removed by the revert"
    );
}

/// Tests that 'undo' with --no-push reverts locally without pushing.
#[test]
#[serial]
fn test_undo_no_push_command() {
    let (_dir, _bare_dir, repo_path) = setup_temp_git_repo();
    std::env::set_current_dir(&repo_path).unwrap();

    std::fs::write(repo_path.join("OOPS.md"), "oops").unwrap();
    std::process::Command::new("git")
        .args(&["add", "."])
        .current_dir(&repo_path)
        .output()
        .unwrap();
    std::process::Command::new("git")
        .args(&["commit", "-m", "feat: oops"])
        .current_dir(&repo_path)
        .output()
        .unwrap();
    std::process::Command::new("git")
        .args(&["push"])
        .current_dir(&repo_path)
        .output()
        .unwrap();

    let sha_output = Command::cargo_bin("tbdflow")
        .unwrap()
        .arg("head-sha")
        .output()
        .unwrap();
    let sha = String::from_utf8_lossy(&sha_output.stdout)
        .trim()
        .to_string();

    let mut cmd = Command::cargo_bin("tbdflow").unwrap();
    cmd.arg("undo").arg(&sha).arg("--no-push");
    cmd.assert()
        .success()
        .stdout(contains("Revert commit created locally (--no-push)"));

    assert!(!repo_path.join("OOPS.md").exists());
}

/// Tests that 'undo' fails gracefully for a non-existent SHA.
#[test]
#[serial]
fn test_undo_nonexistent_sha() {
    let (_dir, _bare_dir, repo_path) = setup_temp_git_repo();
    std::env::set_current_dir(&repo_path).unwrap();

    let mut cmd = Command::cargo_bin("tbdflow").unwrap();
    cmd.arg("undo").arg("deadbeefdeadbeef");
    cmd.assert().failure();
}

/// Tests that the 'check-branches' lists and warns about branches that are stale (older than 1 day)
#[test]
#[serial]
fn test_check_branches_command() {
    let (_dir, _bare_dir, repo_path) = setup_temp_git_repo();
    std::env::set_current_dir(&repo_path).unwrap();

    // Create a feature branch that is stale
    let mut create_cmd = Command::cargo_bin("tbdflow").unwrap();
    create_cmd
        .arg("branch")
        .arg("--type")
        .arg("feature")
        .arg("--name")
        .arg("stale-feature");
    create_cmd.assert().success();

    let old_date = (Utc::now() - Duration::hours(25)).to_rfc3339();

    std::process::Command::new("git")
        .args(&["commit", "--allow-empty", "-m", "stale commit"])
        .env("GIT_COMMITTER_DATE", &old_date) // Set the committer date
        .current_dir(&repo_path)
        .output()
        .unwrap();
    // Switch to main branch to ensure the stale branch is not the current one
    std::process::Command::new("git")
        .args(&["checkout", "main"])
        .current_dir(&repo_path)
        .output()
        .unwrap();

    let mut cmd = Command::cargo_bin("tbdflow").unwrap();
    cmd.arg("check-branches");
    cmd.assert()
        .success()
        .stdout(contains("Warning: The following branches may be stale:"))
        .stdout(contains("feature_stale-feature"));
}

/// Tests that the radar command shows disabled message when radar is not enabled.
#[test]
#[serial]
fn test_radar_disabled_by_default() {
    let (_dir, _bare_dir, repo_path) = setup_temp_git_repo();
    std::env::set_current_dir(&repo_path).unwrap();

    let config_content = r#"
main_branch_name: main
stale_branch_threshold_days: 1
branch_types:
  feat: "feat/"
automatic_tags:
  release_prefix: "v"
"#;
    std::fs::write(repo_path.join(".tbdflow.yml"), config_content).unwrap();

    let mut cmd = Command::cargo_bin("tbdflow").unwrap();
    cmd.arg("radar");
    cmd.assert().success().stdout(contains("Radar is disabled"));
}

/// Tests that the radar command runs successfully when enabled with no overlaps.
#[test]
#[serial]
fn test_radar_no_overlaps() {
    let (_dir, _bare_dir, repo_path) = setup_temp_git_repo();
    std::env::set_current_dir(&repo_path).unwrap();

    let config_content = r#"
main_branch_name: main
stale_branch_threshold_days: 1
radar:
  enabled: true
  level: file
branch_types:
  feat: "feat/"
automatic_tags:
  release_prefix: "v"
"#;
    std::fs::write(repo_path.join(".tbdflow.yml"), config_content).unwrap();
    // Commit the config so working directory is clean for radar
    std::process::Command::new("git")
        .args(&["add", ".tbdflow.yml"])
        .current_dir(&repo_path)
        .output()
        .unwrap();
    std::process::Command::new("git")
        .args(&["commit", "-m", "chore: add config"])
        .current_dir(&repo_path)
        .output()
        .unwrap();
    std::process::Command::new("git")
        .args(&["push"])
        .current_dir(&repo_path)
        .output()
        .unwrap();

    // Modify an existing tracked file so radar has local changes to scan
    std::fs::write(repo_path.join("README.md"), "modified locally").unwrap();

    let mut cmd = Command::cargo_bin("tbdflow").unwrap();
    cmd.arg("radar");
    cmd.assert()
        .success()
        .stdout(contains("No overlaps detected"));
}

/// Tests that the radar command detects file-level overlaps with an active remote branch.
#[test]
#[serial]
fn test_radar_detects_file_overlap() {
    let (_dir, _bare_dir, repo_path) = setup_temp_git_repo();
    std::env::set_current_dir(&repo_path).unwrap();

    let config_content = r#"
main_branch_name: main
stale_branch_threshold_days: 1
radar:
  enabled: true
  level: file
branch_types:
  feat: "feat/"
automatic_tags:
  release_prefix: "v"
"#;
    std::fs::write(repo_path.join(".tbdflow.yml"), config_content).unwrap();
    // Commit the config so it's part of the repo
    std::process::Command::new("git")
        .args(&["add", ".tbdflow.yml"])
        .current_dir(&repo_path)
        .output()
        .unwrap();
    std::process::Command::new("git")
        .args(&["commit", "-m", "chore: add config"])
        .current_dir(&repo_path)
        .output()
        .unwrap();
    std::process::Command::new("git")
        .args(&["push"])
        .current_dir(&repo_path)
        .output()
        .unwrap();

    // Create a feature branch, modify README.md, push it
    std::process::Command::new("git")
        .args(&["checkout", "-b", "feat/other-work"])
        .current_dir(&repo_path)
        .output()
        .unwrap();
    std::fs::write(repo_path.join("README.md"), "changed by teammate").unwrap();
    std::process::Command::new("git")
        .args(&["add", "."])
        .current_dir(&repo_path)
        .output()
        .unwrap();
    std::process::Command::new("git")
        .args(&["commit", "-m", "feat: teammate changes"])
        .current_dir(&repo_path)
        .output()
        .unwrap();
    std::process::Command::new("git")
        .args(&["push", "-u", "origin", "feat/other-work"])
        .current_dir(&repo_path)
        .output()
        .unwrap();

    // Switch back to main and make a local change to the same file
    std::process::Command::new("git")
        .args(&["checkout", "main"])
        .current_dir(&repo_path)
        .output()
        .unwrap();
    std::fs::write(repo_path.join("README.md"), "my local change").unwrap();

    let mut cmd = Command::cargo_bin("tbdflow").unwrap();
    cmd.arg("radar");
    cmd.assert()
        .success()
        .stdout(contains("OVERLAP DETECTED"))
        .stdout(contains("README.md"))
        .stdout(contains("feat/other-work"));
}

/// Tests that --message-file reads the commit subject from a file.
#[test]
#[serial]
fn test_commit_with_message_file() {
    let (_dir, _bare_dir, repo_path) = setup_temp_git_repo();
    std::env::set_current_dir(&repo_path).unwrap();

    // Create a file to commit
    std::fs::write(repo_path.join("FEATURE.md"), "new feature").unwrap();

    // Write the commit subject to a file
    let msg_file = repo_path.join("commit-msg.txt");
    std::fs::write(&msg_file, "add feature file\n").unwrap();

    let mut cmd = Command::cargo_bin("tbdflow").unwrap();
    cmd.arg("commit")
        .arg("--type")
        .arg("feat")
        .arg("--message-file")
        .arg(msg_file.to_str().unwrap())
        .arg("--no-verify");
    cmd.assert()
        .success()
        .stdout(contains("feat: add feature file"))
        .stdout(is_match(r"Successfully (?:committed and )?pushed changes").unwrap());
}

/// Tests that --body-file reads the commit body from a file.
#[test]
#[serial]
fn test_commit_with_body_file() {
    let (_dir, _bare_dir, repo_path) = setup_temp_git_repo();
    std::env::set_current_dir(&repo_path).unwrap();

    // Create a file to commit
    std::fs::write(repo_path.join("DOCS.md"), "documentation").unwrap();

    // Write the commit body to a file
    let body_file = repo_path.join("commit-body.txt");
    std::fs::write(
        &body_file,
        "This adds comprehensive documentation\nfor the new API endpoints.",
    )
    .unwrap();

    let mut cmd = Command::cargo_bin("tbdflow").unwrap();
    cmd.arg("commit")
        .arg("--type")
        .arg("docs")
        .arg("--message")
        .arg("add api docs")
        .arg("--body-file")
        .arg(body_file.to_str().unwrap())
        .arg("--no-verify");
    cmd.assert()
        .success()
        .stdout(contains("This adds comprehensive documentation"))
        .stdout(is_match(r"Successfully (?:committed and )?pushed changes").unwrap());
}

/// Tests that --message-file and --message conflict.
#[test]
#[serial]
fn test_commit_message_file_conflicts_with_message() {
    let (_dir, _bare_dir, repo_path) = setup_temp_git_repo();
    std::env::set_current_dir(&repo_path).unwrap();

    let msg_file = repo_path.join("msg.txt");
    std::fs::write(&msg_file, "subject").unwrap();

    let mut cmd = Command::cargo_bin("tbdflow").unwrap();
    cmd.arg("commit")
        .arg("--type")
        .arg("feat")
        .arg("--message")
        .arg("inline subject")
        .arg("--message-file")
        .arg(msg_file.to_str().unwrap());
    cmd.assert()
        .failure()
        .stderr(contains("cannot be used with"));
}

/// Tests that --body-file and --body conflict.
#[test]
#[serial]
fn test_commit_body_file_conflicts_with_body() {
    let (_dir, _bare_dir, repo_path) = setup_temp_git_repo();
    std::env::set_current_dir(&repo_path).unwrap();

    let body_file = repo_path.join("body.txt");
    std::fs::write(&body_file, "file body").unwrap();

    let mut cmd = Command::cargo_bin("tbdflow").unwrap();
    cmd.arg("commit")
        .arg("--type")
        .arg("feat")
        .arg("--message")
        .arg("subject")
        .arg("--body")
        .arg("inline body")
        .arg("--body-file")
        .arg(body_file.to_str().unwrap());
    cmd.assert()
        .failure()
        .stderr(contains("cannot be used with"));
}
