// Copyright 2022 Google LLC
// Copyright 2026 Bradford Hovinen <bradford@hovinen.me>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use crate::description::Description;
use crate::matcher::{Describable, Matcher, MatcherResult};
use crate::matcher_support::edit_distance;
use crate::matcher_support::summarize_diff::create_diff;
use std::fmt::Debug;

/// Matches a value equal (in the sense of `==`) to `expected`.
///
/// The type of `expected` must implement the [`PartialEq`] trait so that the
/// expected and actual values can be compared.
///
/// ```
/// # use test_that::prelude::*;
/// # fn should_pass() -> TestResult<()> {
/// verify_that!(123, eq(123))?; // Passes
/// #     Ok(())
/// # }
/// # fn should_fail() -> TestResult<()> {
/// verify_that!(123, eq(234))?; // Fails
/// #     Ok(())
/// # }
/// # should_pass().unwrap();
/// # should_fail().unwrap_err();
/// ```
///
/// `expected` to `actual` must be comparable with one another via the
/// [`PartialEq`] trait. In most cases, this means that they must be of the same
/// type. However, there are a few cases where different but closely related
/// types are comparable, for example `String` with `&str`.
///
/// ```
/// # use test_that::prelude::*;
/// # fn should_pass() -> TestResult<()> {
/// verify_that!(String::from("Some value"), eq("Some value"))?; // Passes
/// #     Ok(())
/// # }
/// # should_pass().unwrap();
/// ```
///
/// In most cases however, one must convert one of the arguments explicitly.
/// This can be surprising when comparing integer types or references.
///
/// ```compile_fail
/// verify_that!(123u32, eq(123u64))?; // Does not compile
/// verify_that!(123u32 as u64, eq(123u64))?; // Passes
/// ```
///
/// ```ignore
/// let actual: &T = ...;
/// let expected: T = T{...};
/// verify_that(actual, eq(expected))?; // Does not compile
/// verify_that(actual, eq(&expected))?; // Compiles
/// ```
///
/// When matching with string types (`&str` and `String`), one can set more
/// options on how equality is checked through the
/// [`StrMatcherConfigurator`][crate::matchers::str_matcher::StrMatcherConfigurator]
/// extension trait, which is implemented for this matcher.
pub fn eq<T>(expected: T) -> __internal::EqMatcher<T> {
    __internal::EqMatcher { expected }
}

pub mod __internal {
    use super::*;

    #[doc(hidden)]
    pub struct EqMatcher<T> {
        pub(crate) expected: T,
    }

    impl<T: Debug, A: Debug + ?Sized + PartialEq<T>> Matcher<A> for EqMatcher<T> {
        fn matches(&self, actual: &A) -> MatcherResult {
            (*actual == self.expected).into()
        }

        fn explain_match(&self, actual: &A) -> Description {
            let expected_debug = format!("{:#?}", self.expected);
            let actual_debug = format!("{:#?}", actual);
            let description = self.describe(self.matches(actual));

            let diff = if is_multiline_string_debug(&actual_debug)
                && is_multiline_string_debug(&expected_debug)
            {
                create_diff(
                    // The two calls below return None if and only if the strings expected_debug
                    // respectively actual_debug are not enclosed in ". The calls to
                    // is_multiline_string_debug above ensure that they are. So the calls cannot
                    // actually return None and unwrap() should not panic.
                    &to_display_output(&actual_debug).unwrap(),
                    &to_display_output(&expected_debug).unwrap(),
                    edit_distance::Mode::Exact,
                )
            } else {
                create_diff(&actual_debug, &expected_debug, edit_distance::Mode::Exact)
            };

            format!("which {description}{diff}").into()
        }
    }

    impl<T: Debug> Describable for EqMatcher<T> {
        fn describe(&self, matcher_result: MatcherResult) -> Description {
            match matcher_result {
                MatcherResult::Match => format!("is equal to {:?}", self.expected).into(),
                MatcherResult::NoMatch => format!("isn't equal to {:?}", self.expected).into(),
            }
        }
    }
}

fn is_multiline_string_debug(string: &str) -> bool {
    string.starts_with('"')
        && string.ends_with('"')
        && !string.contains('\n')
        && string.contains("\\n")
}

fn to_display_output(string: &str) -> Option<String> {
    Some(string.strip_prefix('"')?.strip_suffix('"')?.split("\\n").collect::<Vec<_>>().join("\n"))
}

#[cfg(test)]
mod tests {
    use super::eq;
    use crate::prelude::*;
    use indoc::indoc;
    use serial_test::serial;

    #[test]
    fn eq_matches_string_reference_with_string_reference() -> TestResult<()> {
        verify_that!("A string", eq("A string"))
    }

    #[test]
    fn eq_matches_owned_string_with_string_reference() -> TestResult<()> {
        let value = "A string".to_string();
        verify_that!(value, eq("A string"))
    }

    #[test]
    fn eq_matches_owned_string_reference_with_string_reference() -> TestResult<()> {
        let value = "A string".to_string();
        verify_that!(&value, eq("A string"))
    }

    #[test]
    fn eq_matches_i32_with_i32() -> TestResult<()> {
        verify_that!(123, eq(123))
    }

    #[test]
    #[serial]
    fn eq_struct_debug_diff() -> TestResult<()> {
        #[derive(Debug, PartialEq)]
        struct Strukt {
            int: i32,
            string: String,
        }

        let result = verify_that!(
            Strukt { int: 123, string: "something".into() },
            eq(Strukt { int: 321, string: "someone".into() })
        );
        verify_that!(
            result,
            err(displays_as(contains_substring(indoc! {
            "
            Actual: Strukt { int: 123, string: \"something\" },
              which isn't equal to Strukt { int: 321, string: \"someone\" }
              Difference(-actual / +expected):
               Strukt {
              -    int: 123,
              +    int: 321,
              -    string: \"something\",
              +    string: \"someone\",
               }
            "})))
        )
    }

    #[test]
    #[serial]
    fn eq_vec_debug_diff() -> TestResult<()> {
        let result = verify_that!(vec![1, 2, 3], eq(vec![1, 3, 4]));
        verify_that!(
            result,
            err(displays_as(contains_substring(indoc! {
            "
            Value of: vec![1, 2, 3]
            Expected: is equal to [1, 3, 4]
            Actual: [1, 2, 3],
              which isn't equal to [1, 3, 4]
              Difference(-actual / +expected):
               [
                   1,
              -    2,
                   3,
              +    4,
               ]
            "})))
        )
    }

    #[test]
    #[serial]
    fn eq_vec_debug_diff_length_mismatch() -> TestResult<()> {
        let result = verify_that!(vec![1, 2, 3, 4, 5], eq(vec![1, 3, 5]));
        verify_that!(
            result,
            err(displays_as(contains_substring(indoc! {
            "
            Value of: vec![1, 2, 3, 4, 5]
            Expected: is equal to [1, 3, 5]
            Actual: [1, 2, 3, 4, 5],
              which isn't equal to [1, 3, 5]
              Difference(-actual / +expected):
               [
                   1,
              -    2,
                   3,
              -    4,
                   5,
               ]
            "})))
        )
    }

    #[test]
    #[serial]
    fn eq_debug_diff_common_lines_omitted() -> TestResult<()> {
        let result = verify_that!((1..50).collect::<Vec<_>>(), eq((3..52).collect::<Vec<_>>()));
        verify_that!(
            result,
            err(displays_as(contains_substring(indoc! {
            "
            ],
              which isn't equal to [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51]
              Difference(-actual / +expected):
               [
              -    1,
              -    2,
                   3,
                   4,
               <---- 43 common lines omitted ---->
                   48,
                   49,
              +    50,
              +    51,
               ]"})))
        )
    }

    #[test]
    #[serial]
    fn eq_debug_diff_5_common_lines_not_omitted() -> TestResult<()> {
        let result = verify_that!((1..8).collect::<Vec<_>>(), eq((3..10).collect::<Vec<_>>()));
        verify_that!(
            result,
            err(displays_as(contains_substring(indoc! {
            "
            Actual: [1, 2, 3, 4, 5, 6, 7],
              which isn't equal to [3, 4, 5, 6, 7, 8, 9]
              Difference(-actual / +expected):
               [
              -    1,
              -    2,
                   3,
                   4,
                   5,
                   6,
                   7,
              +    8,
              +    9,
               ]"})))
        )
    }

    #[test]
    #[serial]
    fn eq_debug_diff_start_common_lines_omitted() -> TestResult<()> {
        let result = verify_that!((1..50).collect::<Vec<_>>(), eq((1..52).collect::<Vec<_>>()));
        verify_that!(
            result,
            err(displays_as(contains_substring(indoc! {
            "
            ],
              which isn't equal to [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51]
              Difference(-actual / +expected):
               [
                   1,
               <---- 46 common lines omitted ---->
                   48,
                   49,
              +    50,
              +    51,
               ]"})))
        )
    }

    #[test]
    #[serial]
    fn eq_debug_diff_end_common_lines_omitted() -> TestResult<()> {
        let result = verify_that!((1..52).collect::<Vec<_>>(), eq((3..52).collect::<Vec<_>>()));
        verify_that!(
            result,
            err(displays_as(contains_substring(indoc! {
            "
            ],
              which isn't equal to [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51]
              Difference(-actual / +expected):
               [
              -    1,
              -    2,
                   3,
                   4,
               <---- 46 common lines omitted ---->
                   51,
               ]"})))
        )
    }

    #[test]
    fn eq_multi_line_string_debug_diff() -> TestResult<()> {
        let result = verify_that!("One\nTwo\nThree", eq("One\nSix\nThree"));
        // TODO: b/257454450 - Make this more useful, by potentially unescaping the
        // line return.
        verify_that!(
            result,
            err(displays_as(contains_substring(indoc! {
            r#"
            Value of: "One\nTwo\nThree"
            Expected: is equal to "One\nSix\nThree"
            Actual: "One\nTwo\nThree",
              which isn't equal to "One\nSix\nThree"
            "#})))
        )
    }

    #[test]
    #[serial]
    fn match_explanation_contains_diff_of_strings_if_more_than_one_line() -> TestResult<()> {
        let result = verify_that!(
            indoc!(
                "
                    First line
                    Second line
                    Third line
                "
            ),
            eq(indoc!(
                "
                    First line
                    Second lines
                    Third line
                "
            ))
        );

        verify_that!(
            result,
            err(displays_as(contains_substring(
                "\
   First line
  -Second line
  +Second lines
   Third line"
            )))
        )
    }

    #[test]
    #[serial]
    fn match_explanation_does_not_show_diff_if_actual_value_is_single_line() -> TestResult<()> {
        let result = verify_that!(
            "First line",
            eq(indoc!(
                "
                    First line
                    Second line
                    Third line
                "
            ))
        );

        verify_that!(
            result,
            err(displays_as(not(contains_substring("Difference(-actual / +expected):"))))
        )
    }

    #[test]
    #[serial]
    fn match_explanation_does_not_show_diff_if_expected_value_is_single_line() -> TestResult<()> {
        let result = verify_that!(
            indoc!(
                "
                    First line
                    Second line
                    Third line
                "
            ),
            eq("First line")
        );

        verify_that!(
            result,
            err(displays_as(not(contains_substring("Difference(-actual / +expected):"))))
        )
    }
}
