// Copyright 2026 International Digital Economy Academy
//
// 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.

///|
fn[T : Show] debug_string(t : T) -> String {
  let buf = StringBuilder(size_hint=50)
  t.output(buf)
  buf.to_string()
}

///|
pub using @debug {assert_eq}

///|
/// Assert two values are not equal using `@debug.Debug` output for diagnostics.
#callsite(autofill(loc))
pub fn[T : Eq + @debug.Debug] assert_not_eq(
  a : T,
  b : T,
  msg? : String,
  loc~ : SourceLoc,
) -> Unit raise {
  if !(a != b) {
    let fail_msg = match msg {
      Some(msg)
      | (None with msg = "`\{@debug.to_string(a)} == \{@debug.to_string(b)}`") =>
        msg
    }
    fail(fail_msg, loc~)
  }
}

///|
/// Assert referential equality of two values.
///
/// Returns Ok if the two arguments are the same object by reference, using
/// `physical_equal`; raise an Error otherwise. Certain objects may be equal by
/// value, but they are different objects in the memory. This function checks
/// the latter.
///
/// # Examples
///
/// ```skip
/// let a = "4" + "2"
/// let b = "4" + "2"
/// @test.same_object(a, a)
/// @test.is_not(a, b)
/// ```
#deprecated("use `@test.assert_same_object` instead", skip_current_package=true)
#callsite(autofill(loc))
pub fn[T : Show] same_object(a : T, b : T, loc~ : SourceLoc) -> Unit raise {
  if !physical_equal(a, b) {
    let a = debug_string(a)
    let b = debug_string(b)
    fail("`\{a} is \{b}`", loc~)
  }
}

///|
/// Assert referential equality of two values using `@debug.Debug` output for
/// diagnostics.
///
/// This is the preferred identity assertion for new code that only implements
/// `@debug.Debug`.
#callsite(autofill(loc))
pub fn[T : @debug.Debug] assert_same_object(
  a : T,
  b : T,
  loc~ : SourceLoc,
) -> Unit raise {
  if !physical_equal(a, b) {
    let a = @debug.to_string(a)
    let b = @debug.to_string(b)
    fail("`\{a} is \{b}`", loc~)
  }
}

///|
/// Assert referential inequality of two values.
///
/// Returns Ok if the two arguments are NOT the same object by reference, using
/// `physical_equal`; raise an Error otherwise. Certain objects may be equal
/// by value, but they are different objects in the memory. This function
/// checks the latter.
///
/// # Examples
///
/// ```skip
/// let a = "4" + "2"
/// let b = "4" + "2"
/// @test.is_not(a, b)
/// @test.same_object(a, a)
/// ```
#deprecated("use `@test.assert_not_same_object` instead", skip_current_package=true)
#callsite(autofill(loc))
#alias(is_not, deprecated)
pub fn[T : Show] not_same_object(a : T, b : T, loc~ : SourceLoc) -> Unit raise {
  if physical_equal(a, b) {
    let a = debug_string(a)
    let b = debug_string(b)
    fail("`!(\{a} is \{b})`", loc~)
  }
}

///|
/// Assert referential inequality of two values using `@debug.Debug` output for
/// diagnostics.
#callsite(autofill(loc))
pub fn[T : @debug.Debug] assert_not_same_object(
  a : T,
  b : T,
  loc~ : SourceLoc,
) -> Unit raise {
  if physical_equal(a, b) {
    let a = @debug.to_string(a)
    let b = @debug.to_string(b)
    fail("`!(\{a} is \{b})`", loc~)
  }
}

///|
/// Expected to be used in test blocks, don't catch this error.
///
/// Produces an error message similar to @builtin.fail.
#callsite(autofill(loc))
pub fn[T] fail(msg : StringView, loc~ : SourceLoc) -> T raise {
  @builtin.fail(msg, loc~)
}

///|
/// Assert that the given callback raises an error.
///
/// Use this when you only care that the callback raises, not what was raised.
/// If you need to inspect the error value, use `@test.expect_error` instead.
///
/// If the callback does not raise, the test fails at the call site of
/// `assert_raise`.
#callsite(autofill(loc))
pub fn[T, E : Error] assert_raise(
  f : () -> T raise E,
  loc~ : SourceLoc,
) -> Unit raise {
  try f() catch {
    _ => ()
  } noraise {
    _ =>
      @builtin.fail(
        "expected error, but program succeed without raising anything",
        loc~,
      )
  }
}

///|
/// Run the given callback and return the error it raises, so the caller can
/// further interrogate it (`inspect`, `guard ... is`, `assert_true`, etc.).
///
/// If the callback does not raise, the test fails at the call site of
/// `expect_error`. If you do not need the error value, prefer
/// `@test.assert_raise`.
#callsite(autofill(loc))
pub fn[T, E : Error] expect_error(
  f : () -> T raise E,
  loc~ : SourceLoc,
) -> E raise {
  try f() catch {
    err => err
  } noraise {
    _ =>
      @builtin.fail(
        "expected error, but program succeed without raising anything",
        loc~,
      )
  }
}

///|
/// Write data to snapshot buffer, use `snapshot` to output.
/// 
/// See also `@test.Test::writeln`
pub fn Test::write(self : Test, obj : &Show) -> Unit {
  self.buffer.write_string(obj.to_string())
}

///|
/// Write data to snapshot buffer and newline, use `snapshot` to output.
/// 
/// See also `@test.Test::write`
pub fn Test::writeln(self : Test, obj : &Show) -> Unit {
  self.write(obj)
  self.buffer.write_char('\n')
}

///|
/// Take a snapshot of the current buffer and write to a file.
/// 
/// ```mbt check
/// test {
///   let t = @test.Test("test.txt")
///   t.writeln("hello")
///   t.snapshot(filename="test.txt") // actual test block end
/// }
/// ```
///
/// Currently it can only be used once and should be used as the last step.
#callsite(autofill(args_loc, loc))
pub fn Test::snapshot(
  self : Test,
  filename~ : String,
  loc~ : SourceLoc,
  args_loc~ : ArgsLoc,
) -> Unit raise SnapshotError {
  let loc = loc.to_json_string()
  let args_loc = args_loc.to_json()
  let actual = self.buffer.to_string().escape()
  let expect = filename.escape()
  // always raise SnapshotError, moon will handle this
  raise SnapshotError(
    (
      $|@SNAPSHOT_TESTING {"loc": \{loc}, "args_loc": \{args_loc}, "expect": \{expect}, "actual": \{actual}, "snapshot": true}
    ),
  )
}

///|
/// Return the name of the test.
pub fn Test::name(self : Self) -> String {
  self.name
}