use super::*;

fn assert_merge(
    merge_options: Option<&MergeOptions>,
    original: &str,
    ours: &str,
    theirs: &str,
    expected: Result<&str, &str>,
    msg: &str,
) {
    let opts = match merge_options {
        None => &Default::default(),
        Some(opts) => opts,
    };
    let solution = opts.merge(original, ours, theirs);

    assert!(
        same_merge(expected, &solution),
        "{msg}\nexpected={expected:#?}\nactual={solution:#?}"
    );

    let expected_bytes = expected.map(str::as_bytes).map_err(str::as_bytes);
    let solution_bytes = opts.merge_bytes(original.as_bytes(), ours.as_bytes(), theirs.as_bytes());

    assert!(
        same_merge_bytes(expected_bytes, &solution_bytes),
        "{msg}\nexpected={expected_bytes:#?}\nactual={solution_bytes:#?}"
    );
}

fn same_merge(expected: Result<&str, &str>, actual: &Result<String, String>) -> bool {
    match (expected, actual) {
        (Ok(expected), Ok(actual)) => expected == actual,
        (Err(expected), Err(actual)) => expected == actual,
        (_, _) => false,
    }
}

fn same_merge_bytes(expected: Result<&[u8], &[u8]>, actual: &Result<Vec<u8>, Vec<u8>>) -> bool {
    match (expected, actual) {
        (Ok(expected), Ok(actual)) => expected == &actual[..],
        (Err(expected), Err(actual)) => expected == &actual[..],
        (_, _) => false,
    }
}

#[test]
fn test_merge() {
    let original = "\
carrots
garlic
onions
salmon
mushrooms
tomatoes
salt
";
    let a = "\
carrots
salmon
mushrooms
tomatoes
garlic
onions
salt
";
    let b = "\
carrots
salmon
garlic
onions
mushrooms
tomatoes
salt
";

    #[rustfmt::skip]
    assert_merge( None, original, original, original, Ok(original), "Equal case #1",);
    assert_merge(None, original, a, a, Ok(a), "Equal case #2");
    assert_merge(None, original, b, b, Ok(b), "Equal case #3");

    let expected = "\
carrots
<<<<<<< ours
salmon
||||||| original
garlic
onions
salmon
=======
salmon
garlic
onions
>>>>>>> theirs
mushrooms
tomatoes
garlic
onions
salt
";

    assert_merge(None, original, a, b, Err(expected), "Single Conflict case");

    let expected = "\
carrots
<<<<<<< ours
salmon
garlic
onions
||||||| original
garlic
onions
salmon
=======
salmon
>>>>>>> theirs
mushrooms
tomatoes
garlic
onions
salt
";

    assert_merge(
        None,
        original,
        b,
        a,
        Err(expected),
        "Reverse Single Conflict case",
    );
}

#[test]
fn test_merge_multiple_conflicts() {
    let original = "\
carrots
garlic
onions
salmon
tomatoes
salt
";
    let a = "\
carrots
salmon
tomatoes
garlic
onions
salt
";
    let b = "\
carrots
salmon
garlic
onions
tomatoes
salt
";
    let expected_myers = "\
carrots
<<<<<<< ours
salmon
||||||| original
garlic
onions
salmon
=======
salmon
garlic
onions
>>>>>>> theirs
tomatoes
garlic
onions
salt
";
    let opts_myers = MergeOptions {
        algorithm: Algorithm::Myers,
        ..Default::default()
    };
    assert_merge(
        Some(&opts_myers),
        original,
        a,
        b,
        Err(expected_myers),
        "Multiple Conflict case",
    );
    let expected_histogram = expected_myers;

    assert_merge(
        None,
        original,
        a,
        b,
        Err(expected_histogram),
        "Multiple Conflict case",
    );

    let expected_myers = "\
carrots
<<<<<<< ours
salmon
garlic
onions
||||||| original
garlic
onions
salmon
=======
salmon
>>>>>>> theirs
tomatoes
garlic
onions
salt
";
    assert_merge(
        Some(&opts_myers),
        original,
        b,
        a,
        Err(expected_myers),
        "Reverse Multiple Conflict case",
    );

    let expected_histogram = expected_myers;
    assert_merge(
        None,
        original,
        b,
        a,
        Err(expected_histogram),
        "Reverse Multiple Conflict case",
    );
}

#[test]
fn diffy_vs_git() {
    let original = "\
void Chunk_copy(Chunk *src, size_t src_start, Chunk *dst, size_t dst_start, size_t n)
{
    if (!Chunk_bounds_check(src, src_start, n)) return;
    if (!Chunk_bounds_check(dst, dst_start, n)) return;

    memcpy(dst->data + dst_start, src->data + src_start, n);
}

int Chunk_bounds_check(Chunk *chunk, size_t start, size_t n)
{
    if (chunk == NULL) return 0;

    return start <= chunk->length && n <= chunk->length - start;
}
";
    let a = "\
int Chunk_bounds_check(Chunk *chunk, size_t start, size_t n)
{
    if (chunk == NULL) return 0;

    return start <= chunk->length && n <= chunk->length - start;
}

void Chunk_copy(Chunk *src, size_t src_start, Chunk *dst, size_t dst_start, size_t n)
{
    if (!Chunk_bounds_check(src, src_start, n)) return;
    if (!Chunk_bounds_check(dst, dst_start, n)) return;

    memcpy(dst->data + dst_start, src->data + src_start, n);
}
";
    let b = "\
void Chunk_copy(Chunk *src, size_t src_start, Chunk *dst, size_t dst_start, size_t n)
{
    if (!Chunk_bounds_check(src, src_start, n)) return;
    if (!Chunk_bounds_check(dst, dst_start, n)) return;

    // copy the bytes
    memcpy(dst->data + dst_start, src->data + src_start, n);
}

int Chunk_bounds_check(Chunk *chunk, size_t start, size_t n)
{
    if (chunk == NULL) return 0;

    return start <= chunk->length && n <= chunk->length - start;
}
";

    let expected_myers = "\
int Chunk_bounds_check(Chunk *chunk, size_t start, size_t n)
{
    if (chunk == NULL) return 0;

<<<<<<< ours
    return start <= chunk->length && n <= chunk->length - start;
||||||| original
    memcpy(dst->data + dst_start, src->data + src_start, n);
=======
    // copy the bytes
    memcpy(dst->data + dst_start, src->data + src_start, n);
>>>>>>> theirs
}

void Chunk_copy(Chunk *src, size_t src_start, Chunk *dst, size_t dst_start, size_t n)
{
    if (!Chunk_bounds_check(src, src_start, n)) return;
    if (!Chunk_bounds_check(dst, dst_start, n)) return;

    memcpy(dst->data + dst_start, src->data + src_start, n);
}
";
    let opts_myers = MergeOptions {
        algorithm: Algorithm::Myers,
        ..Default::default()
    };
    assert_merge(
        Some(&opts_myers),
        original,
        a,
        b,
        Err(expected_myers),
        "Myers diffy merge",
    );

    let expected_histogram = "\
int Chunk_bounds_check(Chunk *chunk, size_t start, size_t n)
{
    if (chunk == NULL) return 0;

    return start <= chunk->length && n <= chunk->length - start;
}

void Chunk_copy(Chunk *src, size_t src_start, Chunk *dst, size_t dst_start, size_t n)
{
    if (!Chunk_bounds_check(src, src_start, n)) return;
    if (!Chunk_bounds_check(dst, dst_start, n)) return;

    // copy the bytes
    memcpy(dst->data + dst_start, src->data + src_start, n);
}
";

    assert_merge(
        None,
        original,
        a,
        b,
        Ok(expected_histogram),
        "Histogram diffy merge",
    );
}

#[test]
fn correct_range_is_used_for_both_case() {
    let base = r#"
class GithubCall(db.Model):

`url`: URL of request Example.`https://api.github.com`
"#;

    let theirs = r#"
class GithubCall(db.Model):

`repo`: String field. Github repository fields. Example: `amitu/python`
"#;

    let ours = r#"
class Call(models.Model):
`body`: String field. The payload of the webhook call from the github.

`repo`: String field. Github repository fields. Example: `amitu/python`
"#;

    let expected = r#"
class Call(models.Model):
`body`: String field. The payload of the webhook call from the github.

`repo`: String field. Github repository fields. Example: `amitu/python`
"#;

    assert_merge(
        None,
        base,
        ours,
        theirs,
        Ok(expected),
        "MergeRange::Both case",
    );
}

#[test]
fn delete_and_insert_conflict() {
    let base = r#"
{
    int a = 2;
}
"#;

    let ours = r#"
{
}
"#;

    let theirs = r#"
{
    int a = 2;
    int b = 3;
}
"#;

    let expected = r#"
{
<<<<<<< ours
||||||| original
    int a = 2;
=======
    int a = 2;
    int b = 3;
>>>>>>> theirs
}
"#;

    assert_merge(
        None,
        base,
        ours,
        theirs,
        Err(expected),
        "MergeRange (Ours::delete, Theirs::insert) conflict",
    );

    let expected = r#"
{
<<<<<<< ours
    int a = 2;
    int b = 3;
||||||| original
    int a = 2;
=======
>>>>>>> theirs
}
"#;

    assert_merge(
        None,
        base,
        theirs,
        ours,
        Err(expected),
        "MergeRange (Theirs::delete, Ours::insert) conflict",
    );
}

#[test]
fn one_line() {
    let base = "[1, 2]";
    let ours = "[1]";
    let theirs = "[2]";

    let expected = "\
<<<<<<< ours
[1]
||||||| original
[1, 2]
=======
[2]
>>>>>>> theirs";

    assert_merge(None, base, ours, theirs, Err(expected), "One-line");

    let expected = "\
<<<<<<< ours
[2]
||||||| original
[1, 2]
=======
[1]
>>>>>>> theirs";

    assert_merge(None, base, theirs, ours, Err(expected), "One-line reverse");
}

#[test]
fn no_newline_at_the_end() {
    // meanings of the used shortenings:
    // - wo              - without final newline
    // - w               - with final newline
    // - expected_w_wo_w - expected from base_w, left_wo, right_w
    let base_wo = "\
object Foo:
    def bar(input: String) = input";
    let base_w = "\
object Foo:
    def bar(input: String) = input
";

    let ours_wo = "\
object Foo:
    def bar(newname: String) = newname";
    let ours_w = "\
object Foo:
    def bar(newname: String) = newname
";

    let theirs_wo = "\
object Foo:
    def baz(input: String) = input";
    let theirs_w = "\
object Foo:
    def baz(input: String) = input
";

    let expected_wo_wo_wo = "\
object Foo:
<<<<<<< ours
    def bar(newname: String) = newname
||||||| original
    def bar(input: String) = input
=======
    def baz(input: String) = input
>>>>>>> theirs";

    assert_merge(
        None,
        base_wo,
        ours_wo,
        theirs_wo,
        Err(expected_wo_wo_wo),
        "without/without/without",
    );

    let expected_wo_w_wo = "\
object Foo:
<<<<<<< ours
    def bar(newname: String) = newname

||||||| original
    def bar(input: String) = input
=======
    def baz(input: String) = input
>>>>>>> theirs";

    assert_merge(
        None,
        base_wo,
        ours_w,
        theirs_wo,
        Err(expected_wo_w_wo),
        "without/with/without",
    );

    // wo_wo_w case should be symmetrical to wo_w_wo

    let expected_wo_w_w = "\
object Foo:
<<<<<<< ours
    def bar(newname: String) = newname

||||||| original
    def bar(input: String) = input
=======
    def baz(input: String) = input

>>>>>>> theirs";

    assert_merge(
        None,
        base_wo,
        ours_w,
        theirs_w,
        Err(expected_wo_w_w),
        "without/with/with",
    );

    let expected_w_wo_wo = "\
object Foo:
<<<<<<< ours
    def bar(newname: String) = newname
||||||| original
    def bar(input: String) = input

=======
    def baz(input: String) = input
>>>>>>> theirs";

    assert_merge(
        None,
        base_w,
        ours_wo,
        theirs_wo,
        Err(expected_w_wo_wo),
        "with/without/without",
    );

    let expected_w_w_wo = "\
object Foo:
<<<<<<< ours
    def bar(newname: String) = newname

||||||| original
    def bar(input: String) = input

=======
    def baz(input: String) = input
>>>>>>> theirs";

    assert_merge(
        None,
        base_w,
        ours_w,
        theirs_wo,
        Err(expected_w_w_wo),
        "with/with/without",
    );

    // w_wo_w should be symmetrical to w_w_wo

    let expected_w_w_w = "\
object Foo:
<<<<<<< ours
    def bar(newname: String) = newname
||||||| original
    def bar(input: String) = input
=======
    def baz(input: String) = input
>>>>>>> theirs
";

    assert_merge(
        None,
        base_w,
        ours_w,
        theirs_w,
        Err(expected_w_w_w),
        "with/with/with",
    );
}
