#[allow(dead_code)]
mod common;

use std::fs;
use std::path::Path;

use common::fixture::{AbcFixture, CapturingReporter};
use outpost_core::ops::push::{PushOptions, StepResult, run};
use outpost_core::ops::status::{ConfigProblem, run_with as status_run_with};
use outpost_core::{AheadBehind, Outpost, OutpostError, OutpostResult, StepKind};

#[test]
fn pu01_push_sends_outpost_branch_to_source_then_origin() {
    let fixture = AbcFixture::new();
    let outpost_path = fixture.add_outpost("C").expect("add C");
    let outpost_oid = fixture
        .commit_file_in_outpost(
            &outpost_path,
            "outpost side",
            "outpost.txt",
            "from outpost\n",
        )
        .expect("outpost commit");
    let outpost = outpost(&fixture, &outpost_path);
    let mut reporter = CapturingReporter::default();

    let report = run(&outpost, PushOptions, &mut reporter).expect("push");

    assert_pushed_commits(&report.outpost_to_source, 1);
    assert_pushed_commits(&report.source_to_origin, 1);
    assert_eq!(
        fixture
            .rev_parse(&fixture.source, "refs/heads/main")
            .expect("source main"),
        outpost_oid
    );
    assert_eq!(
        fixture
            .rev_parse(&fixture.upstream, "refs/heads/main")
            .expect("origin main"),
        outpost_oid
    );
}

#[test]
fn pu02_push_records_outpost_push_and_source_push_events() {
    let fixture = AbcFixture::new();
    let outpost_path = fixture.add_outpost("C").expect("add C");
    fixture
        .commit_in_outpost(&outpost_path, "outpost side")
        .expect("outpost commit");
    let outpost = outpost(&fixture, &outpost_path);
    let mut reporter = CapturingReporter::default();

    run(&outpost, PushOptions, &mut reporter).expect("push");

    assert_eq!(
        reporter.step_kinds(),
        vec![StepKind::OutpostPush, StepKind::SourcePush]
    );
    assert!(
        reporter.warnings.is_empty(),
        "push should not warn on baseline path: {:?}",
        reporter.warnings
    );
}

#[test]
fn pu03_push_from_outpost_only_branch_returns_ambiguous_branch_creation() {
    let fixture = AbcFixture::new();
    let outpost_path = fixture.add_outpost("C").expect("add C");
    fixture
        .invoker(&outpost_path)
        .run_check(["switch", "-c", "feature/outpost-only"])
        .expect("create outpost-only branch");
    fixture
        .commit_in_outpost(&outpost_path, "outpost-only side")
        .expect("outpost commit");
    let outpost = outpost(&fixture, &outpost_path);
    let mut reporter = CapturingReporter::default();

    let err = expect_error(
        run(&outpost, PushOptions, &mut reporter),
        "push should reject outpost-only branch creation",
    );

    assert!(
        matches!(err, OutpostError::AmbiguousBranchCreation { branch } if branch == "feature/outpost-only")
    );
    assert!(reporter.steps.is_empty());
    assert!(
        fixture
            .rev_parse(&fixture.source, "refs/heads/feature/outpost-only")
            .is_err(),
        "source branch should not be created"
    );
}

#[test]
fn pu04_push_when_source_diverged_from_outpost_returns_divergence() {
    let fixture = AbcFixture::new();
    let outpost_path = fixture.add_outpost("C").expect("add C");
    fixture
        .commit_file_in_outpost(
            &outpost_path,
            "outpost side",
            "outpost.txt",
            "from outpost\n",
        )
        .expect("outpost commit");
    fixture
        .commit_file_in_source("source side", "source.txt", "from source\n")
        .expect("source commit");
    let source_before = fixture
        .rev_parse(&fixture.source, "refs/heads/main")
        .expect("source before");
    let origin_before = fixture
        .rev_parse(&fixture.upstream, "refs/heads/main")
        .expect("origin before");
    let outpost = outpost(&fixture, &outpost_path);
    let mut reporter = CapturingReporter::default();

    let err = expect_error(
        run(&outpost, PushOptions, &mut reporter),
        "push should reject outpost/source divergence",
    );

    assert!(matches!(err, OutpostError::Divergence { branch } if branch == "main"));
    assert!(reporter.steps.is_empty());
    assert_eq!(
        fixture
            .rev_parse(&fixture.source, "refs/heads/main")
            .expect("source after"),
        source_before
    );
    assert_eq!(
        fixture
            .rev_parse(&fixture.upstream, "refs/heads/main")
            .expect("origin after"),
        origin_before
    );
}

#[test]
fn push_when_outpost_is_behind_source_returns_divergence_before_push() {
    let fixture = AbcFixture::new();
    let outpost_path = fixture.add_outpost("C").expect("add C");
    let source_oid = fixture
        .commit_file_in_source("source side", "source.txt", "from source\n")
        .expect("source commit");
    let origin_before = fixture
        .rev_parse(&fixture.upstream, "refs/heads/main")
        .expect("origin before");
    let outpost = outpost(&fixture, &outpost_path);
    let mut reporter = CapturingReporter::default();

    let err = expect_error(
        run(&outpost, PushOptions, &mut reporter),
        "push should reject outpost behind source before pushing",
    );

    assert!(matches!(err, OutpostError::Divergence { branch } if branch == "main"));
    assert!(reporter.steps.is_empty());
    assert_eq!(
        fixture
            .rev_parse(&fixture.source, "refs/heads/main")
            .expect("source after"),
        source_oid
    );
    assert_eq!(
        fixture
            .rev_parse(&fixture.upstream, "refs/heads/main")
            .expect("origin after"),
        origin_before
    );
}

#[test]
fn push_when_origin_is_ahead_returns_divergence_before_source_mutation() {
    let fixture = AbcFixture::new();
    let outpost_path = fixture.add_outpost("C").expect("add C");
    fixture
        .commit_file_in_outpost(
            &outpost_path,
            "outpost side",
            "outpost.txt",
            "from outpost\n",
        )
        .expect("outpost commit");
    let source_before = fixture
        .rev_parse(&fixture.source, "refs/heads/main")
        .expect("source before");
    let origin_oid = fixture
        .commit_file_in_upstream("main", "origin side", "origin.txt", "from origin\n")
        .expect("origin commit");
    let outpost = outpost(&fixture, &outpost_path);
    let mut reporter = CapturingReporter::default();

    let err = expect_error(
        run(&outpost, PushOptions, &mut reporter),
        "push should reject origin divergence before mutating source",
    );

    assert!(matches!(err, OutpostError::Divergence { branch } if branch == "main"));
    assert!(reporter.steps.is_empty());
    assert_eq!(
        fixture
            .rev_parse(&fixture.source, "refs/heads/main")
            .expect("source after"),
        source_before
    );
    assert_eq!(
        fixture
            .rev_parse(&fixture.upstream, "refs/heads/main")
            .expect("origin after"),
        origin_oid
    );
}

#[test]
fn pu05_push_dirty_outpost_succeeds() {
    let fixture = AbcFixture::new();
    let outpost_path = fixture.add_outpost("C").expect("add C");
    let outpost_oid = fixture
        .commit_file_in_outpost(
            &outpost_path,
            "committed outpost side",
            "committed.txt",
            "committed\n",
        )
        .expect("outpost commit");
    fs::write(outpost_path.join("dirty.txt"), "dirty\n").expect("dirty outpost");
    let outpost = outpost(&fixture, &outpost_path);
    let mut reporter = CapturingReporter::default();

    let report = run(&outpost, PushOptions, &mut reporter).expect("push");

    assert_pushed_commits(&report.outpost_to_source, 1);
    assert_pushed_commits(&report.source_to_origin, 1);
    assert_eq!(
        fixture
            .rev_parse(&fixture.source, "refs/heads/main")
            .expect("source main"),
        outpost_oid
    );
    assert_eq!(
        fixture
            .rev_parse(&fixture.upstream, "refs/heads/main")
            .expect("origin main"),
        outpost_oid
    );
}

#[test]
fn push_first_publication_to_absent_origin_branch_counts_only_new_commits() {
    let fixture = AbcFixture::new();
    let feature = fixture
        .create_source_branch("feature/first-publication")
        .expect("create source branch");
    let outpost_path = fixture
        .add_outpost_on_branch("C", Some(feature.clone()))
        .expect("add feature outpost");
    let outpost_oid = fixture
        .commit_file_in_outpost(&outpost_path, "feature side", "feature.txt", "feature\n")
        .expect("outpost commit");
    assert!(
        fixture
            .rev_parse(&fixture.upstream, "refs/heads/feature/first-publication")
            .is_err(),
        "origin feature branch should start absent"
    );
    let outpost = outpost(&fixture, &outpost_path);
    let mut reporter = CapturingReporter::default();

    let report = run(&outpost, PushOptions, &mut reporter).expect("push");

    assert_pushed_commits(&report.outpost_to_source, 1);
    assert_pushed_commits(&report.source_to_origin, 1);
    assert_eq!(
        fixture
            .rev_parse(&fixture.source, "refs/heads/feature/first-publication")
            .expect("source feature"),
        outpost_oid
    );
    assert_eq!(
        fixture
            .rev_parse(&fixture.upstream, "refs/heads/feature/first-publication")
            .expect("origin feature"),
        outpost_oid
    );
    let upstream = fixture
        .source_repo()
        .expect("source repo")
        .upstream_for(&feature)
        .expect("source upstream")
        .expect("feature branch upstream");
    assert_eq!(upstream.remote.as_str(), "origin");
    assert_eq!(
        upstream.merge_ref.as_str(),
        "refs/heads/feature/first-publication"
    );

    let status = status_run_with(&outpost_path, &fixture.git_env).expect("status after push");
    assert!(
        !status
            .problems
            .contains(&ConfigProblem::NoUpstreamTracking {
                branch: feature.clone(),
            }),
        "push should leave source branch tracking origin"
    );
    assert_eq!(
        status.source_ahead_behind_upstream,
        Some(AheadBehind {
            ahead: 0,
            behind: 0,
        })
    );
}

#[test]
fn pu06_push_with_missing_source_returns_source_missing() {
    let fixture = AbcFixture::new();
    let outpost_path = fixture.add_outpost("C").expect("add C");
    fixture
        .commit_in_outpost(&outpost_path, "outpost side")
        .expect("outpost commit");
    let expected_source = fs::canonicalize(&fixture.source).expect("canonical source");
    let moved_source = fixture.root.join("B.moved");
    fs::rename(&fixture.source, &moved_source).expect("move source repo");
    let outpost = outpost(&fixture, &outpost_path);
    let mut reporter = CapturingReporter::default();

    let err = expect_error(
        run(&outpost, PushOptions, &mut reporter),
        "push should reject missing source",
    );

    assert!(matches!(err, OutpostError::SourceMissing(path) if path == expected_source));
    assert!(reporter.steps.is_empty());
}

#[test]
fn pu07_push_uses_custom_remote_for_outpost_to_source_and_origin_for_source_to_upstream() {
    let fixture = AbcFixture::new();
    let outpost_path = fixture
        .add_outpost_with_remote("C", "custom")
        .expect("add custom outpost");
    assert!(
        fixture
            .invoker(&outpost_path)
            .run_capture(["remote", "get-url", "local"])
            .is_err(),
        "custom outpost should not rely on a local remote"
    );
    let outpost_oid = fixture
        .commit_file_in_outpost(
            &outpost_path,
            "custom outpost side",
            "custom.txt",
            "custom\n",
        )
        .expect("outpost commit");
    let outpost = outpost(&fixture, &outpost_path);
    let mut reporter = CapturingReporter::default();

    let report = run(&outpost, PushOptions, &mut reporter).expect("push");

    assert_pushed_commits(&report.outpost_to_source, 1);
    assert_pushed_commits(&report.source_to_origin, 1);
    assert_eq!(
        fixture
            .rev_parse(&fixture.source, "refs/heads/main")
            .expect("source main"),
        outpost_oid
    );
    assert_eq!(
        fixture
            .rev_parse(&fixture.upstream, "refs/heads/main")
            .expect("origin main"),
        outpost_oid
    );
}

#[test]
fn pu08_push_into_dirty_checked_out_source_branch_surfaces_update_instead_git_failed() {
    let fixture = AbcFixture::new();
    fixture
        .commit_file_in_source("baseline tracked file", "tracked.txt", "base\n")
        .expect("source baseline commit");
    let outpost_path = fixture.add_outpost("C").expect("add C");
    fixture
        .commit_file_in_outpost(
            &outpost_path,
            "outpost side",
            "outpost.txt",
            "from outpost\n",
        )
        .expect("outpost commit");
    fs::write(fixture.source.join("tracked.txt"), "dirty source\n").expect("dirty source");
    let origin_before = fixture
        .rev_parse(&fixture.upstream, "refs/heads/main")
        .expect("origin before");
    let outpost = outpost(&fixture, &outpost_path);
    let mut reporter = CapturingReporter::default();

    let err = expect_error(
        run(&outpost, PushOptions, &mut reporter),
        "push should surface Git's updateInstead failure",
    );

    match err {
        OutpostError::GitFailed { stderr, .. } => {
            assert!(
                !stderr.is_empty(),
                "GitFailed should preserve stderr from updateInstead failure"
            );
        }
        other => panic!("expected GitFailed, got {other:?}"),
    }
    assert_eq!(reporter.step_kinds(), vec![StepKind::OutpostPush]);
    assert_eq!(
        fixture
            .rev_parse(&fixture.upstream, "refs/heads/main")
            .expect("origin after"),
        origin_before
    );
}

#[test]
fn pu09_push_with_deny_current_branch_refuse_returns_push_into_checked_out_branch() {
    let fixture = AbcFixture::new();
    let outpost_path = fixture.add_outpost("C").expect("add C");
    fixture
        .commit_in_outpost(&outpost_path, "outpost side")
        .expect("outpost commit");
    fixture
        .invoker(&fixture.source)
        .run_check(["config", "--local", "receive.denyCurrentBranch", "refuse"])
        .expect("set denyCurrentBranch");
    let source_before = fixture
        .rev_parse(&fixture.source, "refs/heads/main")
        .expect("source before");
    let outpost = outpost(&fixture, &outpost_path);
    let mut reporter = CapturingReporter::default();

    let err = expect_error(
        run(&outpost, PushOptions, &mut reporter),
        "push should reject checked-out source branch when denyCurrentBranch refuses",
    );

    assert!(
        matches!(err, OutpostError::PushIntoCheckedOutBranch { source, branch } if source == canonical_source(&fixture) && branch == "main")
    );
    assert!(reporter.steps.is_empty());
    assert_eq!(
        fixture
            .rev_parse(&fixture.source, "refs/heads/main")
            .expect("source after"),
        source_before
    );
}

#[test]
fn pu10_push_on_detached_head_returns_no_upstream_tracking_head_before_push() {
    let fixture = AbcFixture::new();
    let outpost_path = fixture.add_outpost("C").expect("add C");
    let source_before = fixture
        .rev_parse(&fixture.source, "refs/heads/main")
        .expect("source before");
    let origin_before = fixture
        .rev_parse(&fixture.upstream, "refs/heads/main")
        .expect("origin before");
    fixture
        .invoker(&outpost_path)
        .run_check(["checkout", "--detach"])
        .expect("detach outpost HEAD");
    let outpost = outpost(&fixture, &outpost_path);
    let mut reporter = CapturingReporter::default();

    let err = expect_error(
        run(&outpost, PushOptions, &mut reporter),
        "push should reject detached HEAD",
    );

    assert!(matches!(err, OutpostError::NoUpstreamTracking { branch } if branch == "HEAD"));
    assert!(reporter.steps.is_empty());
    assert_eq!(
        fixture
            .rev_parse(&fixture.source, "refs/heads/main")
            .expect("source after"),
        source_before
    );
    assert_eq!(
        fixture
            .rev_parse(&fixture.upstream, "refs/heads/main")
            .expect("origin after"),
        origin_before
    );
}

fn outpost(fixture: &AbcFixture, path: &Path) -> Outpost {
    Outpost::at_with(path, &fixture.git_env).expect("open outpost")
}

fn assert_pushed_commits(result: &StepResult, expected: u32) {
    match result {
        StepResult::Pushed { commits } => assert_eq!(*commits, expected),
    }
}

fn canonical_source(fixture: &AbcFixture) -> std::path::PathBuf {
    fs::canonicalize(&fixture.source).expect("canonical source")
}

fn expect_error<T>(result: OutpostResult<T>, message: &str) -> OutpostError {
    match result {
        Ok(_) => panic!("{message}"),
        Err(err) => err,
    }
}
