// SPDX-FileCopyrightText: 2024 Olivier Charvin <git@olivier.pfad.fr>
//
// SPDX-License-Identifier: CC0-1.0

/*
Package check is a minimalist Go assertion package.

Although [assert libraries are frowned upon] by the Go team, this package aims at reducing the boilerplate while not getting in the way of your tests. Importantly:
  - the tests will [keep going], unless specifically asked not to (to prevent a panic, for instance)
  - the failure message can be expanded (to [identify the function and input] or [print diffs]), without polluting the output on equality

Dedicated to the public domain.

[assert libraries are frowned upon]: https://go.dev/wiki/TestComments#assert-libraries
[keep going]: https://go.dev/wiki/TestComments#keep-going
[identify the function and input]: https://go.dev/wiki/TestComments#identify-the-function
[print diffs]: https://go.dev/wiki/TestComments#print-diffs
*/
package check

import (
	"fmt"
	"reflect"
	"slices"
	"testing"
)

func failureMsg(intro, want, got string) string {
	if len(want) < 10 && len(got) < 10 {
		return intro + ": want " + want + ", got " + got
	}
	return intro + ":" +
		"\nwant: " + want +
		"\ngot : " + got
}

// True calls t.Error with the given args as message if got is not true.
func True(t testing.TB, got bool, args ...any) *Failure {
	t.Helper()
	if !got {
		t.Error(args...)
		return &Failure{t}
	}
	return nil
}

// Equal calls t.Error if want != got.
func Equal[T comparable](t testing.TB, want, got T) *Failure {
	t.Helper()
	if want != got {
		t.Error(failureMsg("Not equal",
			fmt.Sprintf("%v", want),
			fmt.Sprintf("%v", got),
		))
		return &Failure{t}
	}
	return nil
}

// EqualDeep calls t.Error if ![reflect.DeepEqual](want, got).
func EqualDeep[T any](t testing.TB, want, got T) *Failure {
	t.Helper()
	if !reflect.DeepEqual(want, got) {
		t.Error(failureMsg("Not deep equal",
			fmt.Sprintf("%#v", want),
			fmt.Sprintf("%#v", got),
		))
		return &Failure{t}
	}
	return nil
}

// EqualSlice is the slice version of [Equal]. Calls t.Error if want != got with an unified diff.
func EqualSlice[T comparable](t testing.TB, want, got []T) *Failure {
	t.Helper()

	// slice.Equal is not strict enough
	if len(want) == 0 {
		if (want == nil && got != nil) || // nil expected
			(want != nil && got == nil) { // empty slice expected
			t.Error(failureMsg("Not expected emptiness",
				fmt.Sprintf("%#v", want),
				fmt.Sprintf("%#v", got),
			))
			return &Failure{t}
		}
	}

	// at least one is not empty, compare quickly
	// (since we expect the test to pass most of the time)
	if slices.Equal(want, got) {
		return nil
	}

	t.Error(newUnifiedDiff(want, got))
	return &Failure{t}
}

// Failure allows logging more information in case of failure. All method calls will be no-op on nil (when the check succeeded).
type Failure struct {
	tb testing.TB
}

// Fatal stops the test execution if the Failure is not nil (no-op otherwise), see [testing.T.FailNow].
func (f *Failure) Fatal() {
	if f != nil {
		f.tb.FailNow()
	}
}

// Log formats its arguments using default formatting, analogous to Println, and records the text in the error log if the Failure is not nil (no-op otherwise).
func (f *Failure) Log(args ...any) *Failure {
	if f != nil {
		f.tb.Helper()
		f.tb.Log(args...)
	}
	return f
}

// Logf formats its arguments according to the format, analogous to Printf, and records the text in the error log if the Failure is not nil (no-op otherwise)
func (f *Failure) Logf(format string, args ...any) *Failure {
	if f != nil {
		f.tb.Helper()
		f.tb.Logf(format, args...)
	}
	return f
}
