use std::process::Command;
use std::sync::mpsc;
use std::thread;

use tempfile::tempdir;

fn madgit(args: &[&str]) -> std::process::Output {
    Command::new(env!("CARGO_BIN_EXE_madgit"))
        .args(args)
        .output()
        .unwrap()
}

fn git_mad(args: &[&str]) -> std::process::Output {
    Command::new(env!("CARGO_BIN_EXE_git-mad"))
        .args(args)
        .output()
        .unwrap()
}

fn run_git(dir: &std::path::Path, args: &[&str]) {
    let output = Command::new("git")
        .args(args)
        .current_dir(dir)
        .output()
        .unwrap();
    assert!(
        output.status.success(),
        "git command failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );
}

fn init_repo(dir: &std::path::Path) {
    init_repo_with_branch(dir, "main");
}

fn init_repo_with_branch(dir: &std::path::Path, branch: &str) {
    run_git(dir, &["init", "-b", branch]);
    std::fs::write(dir.join("README.md"), "madgit\n").unwrap();
    run_git(dir, &["add", "README.md"]);
    run_git(
        dir,
        &[
            "-c",
            "user.name=Test User",
            "-c",
            "user.email=test@example.com",
            "commit",
            "-m",
            "initial",
        ],
    );
}

fn init_parent_with_repo() -> tempfile::TempDir {
    let temp_dir = tempdir().unwrap();
    let repo_dir = temp_dir.path().join("repo-a");
    std::fs::create_dir(&repo_dir).unwrap();
    init_repo(&repo_dir);
    temp_dir
}

fn clone_repo(source: &std::path::Path, target: &std::path::Path) {
    run_git(
        target.parent().unwrap(),
        &[
            "clone",
            source.to_string_lossy().as_ref(),
            target.to_string_lossy().as_ref(),
        ],
    );
}

fn spawn_stub_server(responses: Vec<(String, String)>) -> (String, thread::JoinHandle<()>) {
    let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
    let address = listener.local_addr().unwrap();
    let base_url = format!("http://{address}");
    let (ready_tx, ready_rx) = mpsc::channel();

    let handle = thread::spawn(move || {
        ready_tx.send(()).unwrap();
        for (expected_path, body) in responses {
            let (mut stream, _) = listener.accept().unwrap();
            let mut buffer = [0; 8192];
            let bytes_read = std::io::Read::read(&mut stream, &mut buffer).unwrap();
            let request = String::from_utf8_lossy(&buffer[..bytes_read]);
            let request_line = request.lines().next().unwrap_or_default();
            let path = request_line.split_whitespace().nth(1).unwrap_or_default();
            assert_eq!(path, expected_path);

            let body = body.replace("{{base_url}}", &format!("http://{address}"));
            let response = format!(
                "HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{}",
                body.len(),
                body
            );
            std::io::Write::write_all(&mut stream, response.as_bytes()).unwrap();
        }
    });

    ready_rx.recv().unwrap();
    (base_url, handle)
}

fn init_bare_remote_with_clone() -> (tempfile::TempDir, std::path::PathBuf, std::path::PathBuf) {
    let temp_dir = tempdir().unwrap();
    let source_dir = temp_dir.path().join("source");
    let remote_dir = temp_dir.path().join("remote.git");
    let clone_dir = temp_dir.path().join("clone");

    std::fs::create_dir(&source_dir).unwrap();
    init_repo(&source_dir);
    run_git(
        temp_dir.path(),
        &["init", "--bare", remote_dir.to_string_lossy().as_ref()],
    );
    run_git(
        &source_dir,
        &[
            "remote",
            "add",
            "origin",
            remote_dir.to_string_lossy().as_ref(),
        ],
    );
    run_git(&source_dir, &["push", "-u", "origin", "main"]);
    clone_repo(&remote_dir, &clone_dir);

    (temp_dir, source_dir, clone_dir)
}

#[test]
fn madgit_help_succeeds() {
    let output = madgit(&["--help"]);

    assert!(output.status.success());
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("Usage: madgit [OPTIONS] <COMMAND>"));
    assert!(
        stdout.contains("Unknown commands are forwarded to Git and executed in each repository")
    );
    assert!(stdout.contains("checkout-main"));
}

#[test]
fn git_mad_help_succeeds() {
    let output = git_mad(&["--help"]);

    assert!(output.status.success());
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(
        stdout.contains("Usage: git-mad [OPTIONS] <COMMAND>")
            || stdout.contains("Usage: madgit [OPTIONS] <COMMAND>")
    );
    assert!(
        stdout.contains("Unknown commands are forwarded to Git and executed in each repository")
    );
}

#[test]
fn repos_command_requires_a_remote_provider() {
    let output = madgit(&["repos"]);

    assert!(!output.status.success());
    assert!(String::from_utf8_lossy(&output.stderr).contains("--github"));
}

#[test]
fn nogit_lists_non_git_subdirectories() {
    let temp_dir = tempdir().unwrap();
    std::fs::create_dir(temp_dir.path().join("repo-a")).unwrap();
    std::fs::create_dir(temp_dir.path().join("repo-a/.git")).unwrap();
    std::fs::create_dir(temp_dir.path().join("notes")).unwrap();

    let temp_dir_arg = temp_dir.path().to_string_lossy().to_string();
    let output = madgit(&["--dir", &temp_dir_arg, "no-git"]);

    assert!(output.status.success());
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("notes"));
    assert!(stdout.contains("Tip: Use 'madgit status' to check repository status"));
}

#[test]
fn nogit_reports_when_every_subdirectory_is_a_git_repository() {
    let temp_dir = tempdir().unwrap();
    std::fs::create_dir(temp_dir.path().join("repo-a")).unwrap();
    std::fs::create_dir(temp_dir.path().join("repo-a/.git")).unwrap();

    let temp_dir_arg = temp_dir.path().to_string_lossy().to_string();
    let output = madgit(&["--dir", &temp_dir_arg, "no-git"]);

    assert!(output.status.success());
    assert!(
        String::from_utf8_lossy(&output.stdout).contains("All subdirectories are git repositories")
    );
}

#[test]
fn status_command_succeeds_for_local_repository_tree() {
    let temp_dir = init_parent_with_repo();

    let temp_dir_arg = temp_dir.path().to_string_lossy().to_string();
    let output = madgit(&["--dir", &temp_dir_arg, "status"]);

    assert!(output.status.success());
    assert!(String::from_utf8_lossy(&output.stdout).contains("On branch main"));
}

#[test]
fn exec_command_runs_inside_each_repository() {
    let temp_dir = init_parent_with_repo();

    let temp_dir_arg = temp_dir.path().to_string_lossy().to_string();
    let output = madgit(&[
        "--dir",
        &temp_dir_arg,
        "exec",
        "--",
        "sh",
        "-c",
        "touch from-exec",
    ]);

    assert!(output.status.success());
    assert!(temp_dir.path().join("repo-a/from-exec").exists());
}

#[test]
fn unsupported_top_level_command_is_forwarded_to_git() {
    let temp_dir = init_parent_with_repo();
    let repo_dir = temp_dir.path().join("repo-a");

    let temp_dir_arg = temp_dir.path().to_string_lossy().to_string();
    let output = madgit(&["--dir", &temp_dir_arg, "rev-parse", "--show-toplevel"]);

    assert!(output.status.success());
    assert!(String::from_utf8_lossy(&output.stdout).contains(repo_dir.to_string_lossy().as_ref()));
}

#[test]
fn git_mad_unsupported_command_is_forwarded_to_git() {
    let temp_dir = init_parent_with_repo();
    let repo_dir = temp_dir.path().join("repo-a");

    let temp_dir_arg = temp_dir.path().to_string_lossy().to_string();
    let output = git_mad(&["--dir", &temp_dir_arg, "rev-parse", "--show-toplevel"]);

    assert!(output.status.success());
    assert!(String::from_utf8_lossy(&output.stdout).contains(repo_dir.to_string_lossy().as_ref()));
}

#[test]
fn stash_list_command_succeeds_for_local_repository_tree() {
    let temp_dir = init_parent_with_repo();

    let temp_dir_arg = temp_dir.path().to_string_lossy().to_string();
    let output = madgit(&["--dir", &temp_dir_arg, "stash", "list"]);

    assert!(output.status.success());
}

#[test]
fn checkout_command_is_forwarded_to_git_when_branch_exists() {
    let temp_dir = init_parent_with_repo();

    let temp_dir_arg = temp_dir.path().to_string_lossy().to_string();
    let output = madgit(&["--dir", &temp_dir_arg, "checkout", "main"]);

    assert!(output.status.success());
    assert!(String::from_utf8_lossy(&output.stderr).contains("Executing: git checkout main"));
}

#[test]
fn checkout_main_command_uses_main_branch_when_present() {
    let temp_dir = init_parent_with_repo();

    let temp_dir_arg = temp_dir.path().to_string_lossy().to_string();
    let output = madgit(&["--dir", &temp_dir_arg, "checkout-main"]);

    assert!(output.status.success());
    assert!(String::from_utf8_lossy(&output.stderr)
        .contains("Checking out default branch (main/master)"));
}

#[test]
fn checkout_main_command_falls_back_to_master_branch() {
    let temp_dir = tempdir().unwrap();
    let repo_dir = temp_dir.path().join("repo-a");
    std::fs::create_dir(&repo_dir).unwrap();
    init_repo_with_branch(&repo_dir, "master");

    let temp_dir_arg = temp_dir.path().to_string_lossy().to_string();
    let output = madgit(&["--dir", &temp_dir_arg, "checkout-main"]);

    assert!(output.status.success());
    let branch = Command::new("git")
        .args(["branch", "--show-current"])
        .current_dir(&repo_dir)
        .output()
        .unwrap();
    assert_eq!(String::from_utf8_lossy(&branch.stdout).trim(), "master");
}

#[test]
fn clone_command_reads_repositories_from_file() {
    let source_dir = tempdir().unwrap();
    let target_dir = tempdir().unwrap();
    let repos_file = target_dir.path().join("repos.txt");

    init_repo(source_dir.path());
    std::fs::write(&repos_file, format!("{}\n", source_dir.path().display())).unwrap();

    let target_dir_arg = target_dir.path().to_string_lossy().to_string();
    let repos_file_arg = repos_file.to_string_lossy().to_string();
    let output = madgit(&["--dir", &target_dir_arg, "clone", "--file", &repos_file_arg]);

    assert!(output.status.success());
    assert!(target_dir
        .path()
        .join(source_dir.path().file_name().unwrap())
        .exists());
}

#[test]
fn stash_defaults_to_push() {
    let temp_dir = init_parent_with_repo();
    let repo_dir = temp_dir.path().join("repo-a");
    std::fs::write(repo_dir.join("README.md"), "updated\n").unwrap();

    let temp_dir_arg = temp_dir.path().to_string_lossy().to_string();
    let output = madgit(&["--dir", &temp_dir_arg, "stash"]);

    assert!(output.status.success());
    let stash_list = Command::new("git")
        .args(["stash", "list"])
        .current_dir(&repo_dir)
        .output()
        .unwrap();
    assert!(String::from_utf8_lossy(&stash_list.stdout).contains("stash@{0}"));
}

#[test]
fn fetch_command_updates_remote_tracking_information() {
    let (_temp_dir, source_dir, clone_dir) = init_bare_remote_with_clone();

    std::fs::write(source_dir.join("later.txt"), "new\n").unwrap();
    run_git(&source_dir, &["add", "later.txt"]);
    run_git(
        &source_dir,
        &[
            "-c",
            "user.name=Test User",
            "-c",
            "user.email=test@example.com",
            "commit",
            "-m",
            "later",
        ],
    );
    run_git(&source_dir, &["push"]);

    let parent_dir = clone_dir.parent().unwrap();
    let parent_dir_arg = parent_dir.to_string_lossy().to_string();
    let output = madgit(&["--dir", &parent_dir_arg, "fetch"]);

    assert!(output.status.success());
    let rev = Command::new("git")
        .args(["rev-parse", "origin/main"])
        .current_dir(&clone_dir)
        .output()
        .unwrap();
    assert!(rev.status.success());
}

#[test]
fn pull_command_brings_clone_up_to_date() {
    let (_temp_dir, source_dir, clone_dir) = init_bare_remote_with_clone();

    std::fs::write(source_dir.join("pulled.txt"), "incoming\n").unwrap();
    run_git(&source_dir, &["add", "pulled.txt"]);
    run_git(
        &source_dir,
        &[
            "-c",
            "user.name=Test User",
            "-c",
            "user.email=test@example.com",
            "commit",
            "-m",
            "incoming",
        ],
    );
    run_git(&source_dir, &["push"]);

    let parent_dir = clone_dir.parent().unwrap();
    let parent_dir_arg = parent_dir.to_string_lossy().to_string();
    let output = madgit(&["--dir", &parent_dir_arg, "pull"]);

    assert!(output.status.success());
    assert!(clone_dir.join("pulled.txt").exists());
}

#[test]
fn push_command_sends_local_commit_to_remote() {
    let (_temp_dir, _source_dir, clone_dir) = init_bare_remote_with_clone();

    std::fs::write(clone_dir.join("pushed.txt"), "outgoing\n").unwrap();
    run_git(&clone_dir, &["add", "pushed.txt"]);
    run_git(
        &clone_dir,
        &[
            "-c",
            "user.name=Test User",
            "-c",
            "user.email=test@example.com",
            "commit",
            "-m",
            "outgoing",
        ],
    );

    let parent_dir = clone_dir.parent().unwrap();
    let parent_dir_arg = parent_dir.to_string_lossy().to_string();
    let output = madgit(&["--dir", &parent_dir_arg, "push"]);

    assert!(output.status.success());
    let verify = Command::new("git")
        .args(["ls-remote", "--heads", "origin", "main"])
        .current_dir(&clone_dir)
        .output()
        .unwrap();
    assert!(String::from_utf8_lossy(&verify.stdout).contains("refs/heads/main"));
}

#[test]
fn repos_command_prints_discovered_remote_repositories() {
    let (base_url, handle) = spawn_stub_server(vec![
        (
            "/api/v1/users/wykwit/repos?page=1&limit=50".to_string(),
            r#"[{"clone_url":"https://example.com/one.git"}]"#.to_string(),
        ),
        (
            "/api/v1/users/wykwit/repos?page=2&limit=50".to_string(),
            "[]".to_string(),
        ),
    ]);

    let forgejo_arg = format!("{base_url}/wykwit");
    let output = madgit(&["repos", "--forgejo", &forgejo_arg]);

    handle.join().unwrap();
    assert!(output.status.success());
    assert!(String::from_utf8_lossy(&output.stdout).contains("https://example.com/one.git"));
}
