use gix_merge::blob::{
    builtin_driver,
    builtin_driver::binary::{Pick, ResolveWith},
    Resolution,
};

#[test]
fn binary() {
    assert_eq!(
        builtin_driver::binary(None),
        (Pick::Ours, Resolution::Conflict),
        "by default it picks ours and marks it as conflict"
    );
    assert_eq!(
        builtin_driver::binary(Some(ResolveWith::Ancestor)),
        (Pick::Ancestor, Resolution::CompleteWithAutoResolvedConflict),
        "Otherwise we can pick anything and it will mark it as complete"
    );
    assert_eq!(
        builtin_driver::binary(Some(ResolveWith::Ours)),
        (Pick::Ours, Resolution::CompleteWithAutoResolvedConflict)
    );
    assert_eq!(
        builtin_driver::binary(Some(ResolveWith::Theirs)),
        (Pick::Theirs, Resolution::CompleteWithAutoResolvedConflict)
    );
}

mod text {
    use arbitrary::Arbitrary;
    use bstr::ByteSlice;
    use gix_merge::blob::{
        builtin_driver,
        builtin_driver::text::{self, Conflict, ConflictStyle},
        Resolution,
    };
    use pretty_assertions::assert_str_eq;
    use std::num::NonZero;

    const DIVERGING: &[&str] = &[
        // Somehow, on in zdiff mode, it's different, and I wasn't able to figure out the rule properly.
        // Now we prefer ancestor/before newlines and somewhat ignore our hunks. It's probably a minor issue in practice.
        // gix: "1\r\n2\n<<<<<<< complex/marker-newline-handling-lf2/ours.blob\n4\r\n||||||| complex/marker-newline-handling-lf2/base.blob\r\n2\r\n3\n=======\n5\n>>>>>>> complex/marker-newline-handling-lf2/theirs.blob\n"
        // git: "1\r\n2\n<<<<<<< complex/marker-newline-handling-lf2/ours.blob\n4  \n||||||| complex/marker-newline-handling-lf2/base.blob  \n2\r\n3\n=======\n5\n>>>>>>> complex/marker-newline-handling-lf2/theirs.blob\n"
        "complex/marker-newline-handling-lf2/zdiff3.merged",
        "complex/marker-newline-handling-lf2/zdiff3-histogram.merged",
        // This is related to Git seemingly extending a hunk to increase overlap (see diff3)
        "zdiff3-interesting/merge.merged",
        "zdiff3-interesting/merge-ours.merged",
        "zdiff3-interesting/diff3.merged",
        "zdiff3-interesting/diff3-histogram.merged",
        "zdiff3-interesting/zdiff3.merged",
        "zdiff3-interesting/zdiff3-histogram.merged",
        "zdiff3-interesting/merge-union.merged",
        // Git can extend hunks, similar to above, but the effect is not as noticeable.
        // Implementing this would be interesting, to figure out when the hunk processing should apply.
        "zdiff3-evil/merge.merged",
        "zdiff3-evil/merge-union.merged",
        // Git seems to merge to hunks if they are close together to get a less noisy diff.
        "zdiff3-middlecommon/merge.merged",
        "zdiff3-middlecommon/merge-union.merged",
        // Git has special character handling, which does magic to prevent conflicts
        "complex/auto-simplification/merge.merged",
        "complex/auto-simplification/merge-union.merged",
        // Git has special newline handling when diffing,
        // which auto-inserts a newline when it was removed, kind of.
        "complex/missing-LF-at-EOF/merge.merged",
        "complex/missing-LF-at-EOF/diff3.merged",
        "complex/missing-LF-at-EOF/diff3-histogram.merged",
        "complex/missing-LF-at-EOF/zdiff3.merged",
        "complex/missing-LF-at-EOF/zdiff3-histogram.merged",
        "complex/missing-LF-at-EOF/merge-ours.merged",
        "complex/missing-LF-at-EOF/merge-theirs.merged",
        "complex/missing-LF-at-EOF/merge-union.merged",
        // Git has different diff-slider-heuristics so diffs can be different.
        // See https://github.com/mhagger/diff-slider-tools.
        "complex/spurious-c-conflicts/merge.merged",
        "complex/spurious-c-conflicts/merge-union.merged",
        "complex/spurious-c-conflicts/diff3-histogram.merged",
        "complex/spurious-c-conflicts/zdiff3-histogram.merged",
    ];

    /// Should be a copy of `DIVERGING` once the reverse operation truly works like before
    const DIVERGING_REVERSED: &[&str] = &[
        // expected cases
        "zdiff3-middlecommon/merge.merged-reversed",
        "zdiff3-middlecommon/merge-union.merged-reversed",
        "zdiff3-interesting/merge.merged-reversed",
        "zdiff3-interesting/merge-theirs.merged-reversed",
        "zdiff3-interesting/diff3.merged-reversed",
        "zdiff3-interesting/diff3-histogram.merged-reversed",
        "zdiff3-interesting/zdiff3.merged-reversed",
        "zdiff3-interesting/zdiff3-histogram.merged-reversed",
        "zdiff3-interesting/merge-union.merged-reversed",
        "zdiff3-evil/merge.merged-reversed",
        "zdiff3-evil/merge-union.merged-reversed",
        "complex/missing-LF-at-EOF/merge.merged-reversed",
        "complex/missing-LF-at-EOF/diff3.merged-reversed",
        "complex/missing-LF-at-EOF/diff3-histogram.merged-reversed",
        "complex/missing-LF-at-EOF/zdiff3.merged-reversed",
        "complex/missing-LF-at-EOF/zdiff3-histogram.merged-reversed",
        "complex/missing-LF-at-EOF/merge-ours.merged-reversed",
        "complex/missing-LF-at-EOF/merge-theirs.merged-reversed",
        "complex/missing-LF-at-EOF/merge-union.merged-reversed",
        "complex/auto-simplification/merge.merged-reversed",
        "complex/auto-simplification/merge-union.merged-reversed",
        "complex/marker-newline-handling-lf2/zdiff3.merged-reversed",
        "complex/marker-newline-handling-lf2/zdiff3-histogram.merged-reversed",
        "complex/spurious-c-conflicts/merge.merged-reversed",
        "complex/spurious-c-conflicts/merge-union.merged-reversed",
        "complex/spurious-c-conflicts/diff3-histogram.merged-reversed",
        "complex/spurious-c-conflicts/zdiff3-histogram.merged-reversed",
    ];

    // TODO: fix all of these eventually
    fn is_case_diverging(case: &baseline::Expectation, diverging: &[&str]) -> bool {
        diverging.iter().any(|name| case.name == *name)
    }

    #[test]
    fn fuzzed() {
        for (ours, base, theirs, opts) in [
            (
                &[255, 10, 10, 255][..],
                &[0, 10, 10, 13, 10, 193, 0, 51, 8, 33][..],
                &[10, 255, 10, 10, 10, 0, 10][..],
                builtin_driver::text::Options {
                    conflict: Conflict::ResolveWithUnion,
                    diff_algorithm: imara_diff::Algorithm::Myers,
                },
            ),
            (
                &[],
                &[10, 255, 255, 255],
                &[255, 10, 255, 10, 10, 255, 40],
                builtin_driver::text::Options::default(),
            ),
        ] {
            let mut out = Vec::new();
            let mut input = imara_diff::InternedInput::default();
            gix_merge::blob::builtin_driver::text(&mut out, &mut input, Default::default(), ours, base, theirs, opts);
        }
    }

    #[derive(Debug, Arbitrary)]
    struct FuzzCtx<'a> {
        base: &'a [u8],
        ours: &'a [u8],
        theirs: &'a [u8],
        marker_size: NonZero<u8>,
    }

    fn run_fuzz_case(ours: &[u8], base: &[u8], theirs: &[u8], marker_size: NonZero<u8>) {
        let mut out = Vec::new();
        let mut input = imara_diff::InternedInput::default();
        // Keep this in sync with the fuzz target. Histogram remains enabled here because it is the
        // diff algorithm we fuzz through gix-merge itself. Myers-family algorithms have
        // pathological cases that are expensive enough under fuzz instrumentation to turn the
        // target into a timeout reproducer for the diff backend instead of a useful gix-merge
        // fuzz harness.
        for (left, right) in [(ours, theirs), (theirs, ours)] {
            input.clear();
            let merge = text::Merge::new(&mut input, left, base, right, imara_diff::Algorithm::Histogram);
            let resolution = merge.run(&mut out, Default::default(), Conflict::default());
            if resolution == Resolution::Conflict {
                for conflict in [
                    Conflict::ResolveWithOurs,
                    Conflict::ResolveWithTheirs,
                    Conflict::ResolveWithUnion,
                    Conflict::Keep {
                        style: ConflictStyle::Diff3,
                        marker_size,
                    },
                    Conflict::Keep {
                        style: ConflictStyle::ZealousDiff3,
                        marker_size,
                    },
                ] {
                    merge.run(&mut out, Default::default(), conflict);
                }
            }
        }
    }

    #[test]
    fn clusterfuzz_timeout_regression() {
        for (name, data) in [
            (
                "clusterfuzz-testcase-minimized-gix-merge-blob-6377298803884032",
                include_bytes!("../../fixtures/clusterfuzz-testcase-minimized-gix-merge-blob-6377298803884032")
                    .as_slice(),
            ),
            (
                "clusterfuzz-testcase-minimized-gix-merge-blob-5577413097750528",
                include_bytes!("../../fixtures/clusterfuzz-testcase-minimized-gix-merge-blob-5577413097750528")
                    .as_slice(),
            ),
        ] {
            let ctx = FuzzCtx::arbitrary(&mut arbitrary::Unstructured::new(data))
                .unwrap_or_else(|_| panic!("{name}: testcase matches the historical fuzz target input layout"));
            run_fuzz_case(ctx.ours, ctx.base, ctx.theirs, ctx.marker_size);
        }
    }

    #[test]
    fn run_baseline() -> crate::Result {
        let root = gix_testtools::scripted_fixture_read_only("text-baseline.sh")?;
        for (baseline, diverging, expected_percentage) in [
            ("baseline.cases", DIVERGING, 10),
            ("baseline-reversed.cases", DIVERGING_REVERSED, 10),
        ] {
            let cases = std::fs::read_to_string(root.join(baseline))?;
            let mut out = Vec::new();
            let mut num_diverging = 0;
            let mut num_cases = 0;
            for case in baseline::Expectations::new(&root, &cases) {
                num_cases += 1;
                let mut input = imara_diff::InternedInput::default();
                let actual = gix_merge::blob::builtin_driver::text(
                    &mut out,
                    &mut input,
                    case.labels(),
                    &case.ours,
                    &case.base,
                    &case.theirs,
                    case.options,
                );
                if is_case_diverging(&case, diverging) {
                    num_diverging += 1;
                } else {
                    if case.expected.contains_str("<<<<<<<") {
                        assert_eq!(actual, Resolution::Conflict, "{}: resolution mismatch", case.name);
                    } else {
                        assert!(
                            matches!(
                                actual,
                                Resolution::Complete | Resolution::CompleteWithAutoResolvedConflict
                            ),
                            "{}: resolution mismatch",
                            case.name
                        );
                    }
                    assert_str_eq!(
                        out.as_bstr().to_str_lossy(),
                        case.expected.to_str_lossy(),
                        "{}: output mismatch\n{}",
                        case.name,
                        out.as_bstr()
                    );
                    assert_eq!(out.as_bstr(), case.expected);
                }
            }

            assert_eq!(
                num_diverging,
                diverging.len(),
                "Number of expected diverging cases must match the actual one - probably the implementation improved"
            );
            assert_eq!(
                ((num_diverging as f32 / num_cases as f32) * 100.0) as usize,
                expected_percentage,
                "Just to show the percentage of skipped tests - this should get better"
            );
        }
        Ok(())
    }

    #[test]
    fn both_sides_same_changes_are_conflict_free() {
        for conflict in [
            builtin_driver::text::Conflict::Keep {
                style: ConflictStyle::Merge,
                marker_size: 7.try_into().unwrap(),
            },
            builtin_driver::text::Conflict::Keep {
                style: ConflictStyle::Diff3,
                marker_size: 7.try_into().unwrap(),
            },
            builtin_driver::text::Conflict::Keep {
                style: ConflictStyle::ZealousDiff3,
                marker_size: 7.try_into().unwrap(),
            },
            builtin_driver::text::Conflict::ResolveWithOurs,
            builtin_driver::text::Conflict::ResolveWithTheirs,
            builtin_driver::text::Conflict::ResolveWithUnion,
        ] {
            let options = builtin_driver::text::Options {
                conflict,
                ..Default::default()
            };
            let mut input = imara_diff::InternedInput::default();
            let mut out = Vec::new();
            let actual = builtin_driver::text(
                &mut out,
                &mut input,
                Default::default(),
                b"1\n3\nother",
                b"1\n2\n3",
                b"1\n3\nother",
                options,
            );
            assert_eq!(actual, Resolution::Complete, "{conflict:?}");
        }
    }

    #[test]
    fn both_differ_partially_resolution_is_conflicting() {
        for (conflict, expected) in [
            (
                builtin_driver::text::Conflict::Keep {
                    style: ConflictStyle::Merge,
                    marker_size: 7.try_into().unwrap(),
                },
                Resolution::Conflict,
            ),
            (
                builtin_driver::text::Conflict::Keep {
                    style: ConflictStyle::Diff3,
                    marker_size: 7.try_into().unwrap(),
                },
                Resolution::Conflict,
            ),
            (
                builtin_driver::text::Conflict::Keep {
                    style: ConflictStyle::ZealousDiff3,
                    marker_size: 7.try_into().unwrap(),
                },
                Resolution::Conflict,
            ),
            (
                builtin_driver::text::Conflict::ResolveWithOurs,
                Resolution::CompleteWithAutoResolvedConflict,
            ),
            (
                builtin_driver::text::Conflict::ResolveWithTheirs,
                Resolution::CompleteWithAutoResolvedConflict,
            ),
            (
                builtin_driver::text::Conflict::ResolveWithUnion,
                Resolution::CompleteWithAutoResolvedConflict,
            ),
        ] {
            let options = builtin_driver::text::Options {
                conflict,
                ..Default::default()
            };
            let mut input = imara_diff::InternedInput::default();
            let mut out = Vec::new();
            let actual = builtin_driver::text(
                &mut out,
                &mut input,
                Default::default(),
                b"1\n3\nours",
                b"1\n2\n3",
                b"1\n3\ntheirs",
                options,
            );
            assert_eq!(actual, expected, "{conflict:?}");
        }
    }

    mod false_conflict {
        use gix_merge::blob::{builtin_driver, builtin_driver::text::Conflict, Resolution};
        use imara_diff::InternedInput;

        /// Minimal reproduction: Myers produces a false conflict where git merge-file resolves cleanly.
        ///
        /// base:   alpha_x / (blank) / bravo_x / charlie_x / (blank)
        /// ours:   (blank) / (blank) / bravo_x / charlie_x
        /// theirs: alpha_x / (blank) / charlie_x / (blank)
        ///
        /// base→ours:  alpha_x deleted (replaced by blank), trailing blank removed
        /// base→theirs: bravo_x deleted
        ///
        /// These are non-overlapping changes that git merges cleanly.
        /// See https://github.com/GitoxideLabs/gitoxide/issues/2475
        #[test]
        fn myers_false_conflict_with_blank_line_ambiguity() {
            let base = b"alpha_x\n\nbravo_x\ncharlie_x\n\n";
            let ours = b"\n\nbravo_x\ncharlie_x\n";
            let theirs = b"alpha_x\n\ncharlie_x\n\n";

            let labels = builtin_driver::text::Labels {
                ancestor: Some("base".into()),
                current: Some("ours".into()),
                other: Some("theirs".into()),
            };

            // Histogram resolves cleanly.
            {
                let options = builtin_driver::text::Options {
                    diff_algorithm: imara_diff::Algorithm::Histogram,
                    conflict: Conflict::Keep {
                        style: builtin_driver::text::ConflictStyle::Merge,
                        marker_size: 7.try_into().unwrap(),
                    },
                };
                let mut out = Vec::new();
                let mut input = InternedInput::default();
                let res = builtin_driver::text(&mut out, &mut input, labels, ours, base, theirs, options);
                assert_eq!(res, Resolution::Complete, "Histogram should resolve cleanly");
            }

            // Myers should also resolve cleanly (it used to produce a false conflict because
            // imara-diff's Myers splits the ours change into two hunks — a deletion at base[0]
            // and an empty insertion at base[2] — and the insertion collided with theirs'
            // deletion at base[2]).
            {
                let options = builtin_driver::text::Options {
                    diff_algorithm: imara_diff::Algorithm::Myers,
                    conflict: Conflict::Keep {
                        style: builtin_driver::text::ConflictStyle::Merge,
                        marker_size: 7.try_into().unwrap(),
                    },
                };
                let mut out = Vec::new();
                let mut input = InternedInput::default();
                let res = builtin_driver::text(&mut out, &mut input, labels, ours, base, theirs, options);
                assert_eq!(
                    res,
                    Resolution::Complete,
                    "Myers should resolve cleanly (git merge-file does). Output:\n{}",
                    String::from_utf8_lossy(&out)
                );
            }
        }
    }

    mod baseline {
        use std::path::Path;

        use bstr::BString;
        use gix_merge::blob::builtin_driver::text::{Conflict, ConflictStyle};

        #[derive(Debug)]
        pub struct Expectation {
            pub ours: BString,
            pub ours_marker: String,
            pub theirs: BString,
            pub theirs_marker: String,
            pub base: BString,
            pub base_marker: String,
            pub name: BString,
            pub expected: BString,
            pub options: gix_merge::blob::builtin_driver::text::Options,
        }

        impl Expectation {
            pub fn labels(&self) -> gix_merge::blob::builtin_driver::text::Labels<'_> {
                gix_merge::blob::builtin_driver::text::Labels {
                    ancestor: Some(self.base_marker.as_str().as_ref()),
                    current: Some(self.ours_marker.as_str().as_ref()),
                    other: Some(self.theirs_marker.as_str().as_ref()),
                }
            }
        }

        pub struct Expectations<'a> {
            root: &'a Path,
            lines: std::str::Lines<'a>,
        }

        impl<'a> Expectations<'a> {
            pub fn new(root: &'a Path, cases: &'a str) -> Self {
                Expectations {
                    root,
                    lines: cases.lines(),
                }
            }
        }

        impl Iterator for Expectations<'_> {
            type Item = Expectation;

            fn next(&mut self) -> Option<Self::Item> {
                let line = self.lines.next()?;
                let mut words = line.split(' ');
                let (Some(ours), Some(base), Some(theirs), Some(output)) =
                    (words.next(), words.next(), words.next(), words.next())
                else {
                    panic!("need at least the input and output")
                };

                let read = |rela_path: &str| read_blob(self.root, rela_path);

                let mut options = gix_merge::blob::builtin_driver::text::Options::default();
                let marker_size = 7.try_into().unwrap();
                for arg in words {
                    options.conflict = match arg {
                        "--diff3" => Conflict::Keep {
                            style: ConflictStyle::Diff3,
                            marker_size,
                        },
                        "--zdiff3" => Conflict::Keep {
                            style: ConflictStyle::ZealousDiff3,
                            marker_size,
                        },
                        "--ours" => Conflict::ResolveWithOurs,
                        "--theirs" => Conflict::ResolveWithTheirs,
                        "--union" => Conflict::ResolveWithUnion,
                        _ => panic!("Unknown argument to parse into options: '{arg}'"),
                    }
                }
                if output.contains("histogram") {
                    options.diff_algorithm = imara_diff::Algorithm::Histogram;
                }

                Some(Expectation {
                    ours: read(ours),
                    ours_marker: ours.into(),
                    theirs: read(theirs),
                    theirs_marker: theirs.into(),
                    base: read(base),
                    base_marker: base.into(),
                    expected: read(output),
                    name: output.into(),
                    options,
                })
            }
        }

        fn read_blob(root: &Path, rela_path: &str) -> BString {
            std::fs::read(root.join(rela_path))
                .unwrap_or_else(|err| panic!("Failed to read '{rela_path}' in '{}': {err}", root.display()))
                .into()
        }
    }
}
