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

package check

import (
	"bytes"
	"fmt"
	"strconv"
	"strings"
	"testing"
)

func TestSuccess(t *testing.T) {
	t.Run("True", func(t *testing.T) {
		f := True(t, true, "hidden msg")

		// all calls below should be a no-op since the previous check succeeded
		f.Log("should not be logged").
			Logf("should not be logged %s", "eiter")
		f.Fatal()
	})
	t.Run("Equal", func(t *testing.T) {
		f := Equal(t, 1, 1)

		// all calls below should be a no-op since the previous check succeeded
		f.Log("should not be logged").
			Logf("should not be logged %s", "eiter")
		f.Fatal()
	})
	t.Run("Equal/error", func(t *testing.T) {
		var err error
		f := Equal(t, nil, err)

		// all calls below should be a no-op since the previous check succeeded
		f.Log("should not be logged").
			Logf("should not be logged %s", "eiter")
		f.Fatal()
	})
	t.Run("EqualDeep", func(t *testing.T) {
		f := EqualDeep(t, map[string]int{"one": 1, "two": 2}, map[string]int{"one": 1, "two": 2})

		// all calls below should be a no-op since the previous check succeeded
		f.Log("should not be logged").
			Logf("should not be logged %s", "eiter")
		f.Fatal()
	})
	t.Run("EqualSlice", func(t *testing.T) {
		f := EqualSlice(t, []string{"one", "two"}, []string{"one", "two"})

		// all calls below should be a no-op since the previous check succeeded
		f.Log("should not be logged").
			Logf("should not be logged %s", "eiter")
		f.Fatal()
	})
	t.Run("EqualSlice/nil/nil", func(t *testing.T) {
		var a []int
		f := EqualSlice(t, a, nil)

		// all calls below should be a no-op since the previous check succeeded
		f.Log("should not be logged").
			Logf("should not be logged %s", "eiter")
		f.Fatal()

		f = EqualSlice(t, nil, a)

		// all calls below should be a no-op since the previous check succeeded
		f.Log("should not be logged").
			Logf("should not be logged %s", "eiter")
		f.Fatal()
	})
	t.Run("EqualSlice/empty/empty", func(t *testing.T) {
		f := EqualSlice(t, []int{}, []int{})

		// all calls below should be a no-op since the previous check succeeded
		f.Log("should not be logged").
			Logf("should not be logged %s", "eiter")
		f.Fatal()
	})
	t.Run("EqualSlice/customType/nativeType", func(t *testing.T) {
		type intSlice []int
		a := intSlice{1}
		f := EqualSlice(t, a, []int{1})

		// all calls below should be a no-op since the previous check succeeded
		f.Log("should not be logged").
			Logf("should not be logged %s", "eiter")
		f.Fatal()
	})
}

type testingTB struct {
	testing.TB
	failNow bool
	errors  []string
	logs    []string
}

func (t *testingTB) Helper() {}
func (t *testingTB) FailNow() {
	t.failNow = true
}

func (t *testingTB) Error(args ...any) {
	t.errors = append(t.errors, fmt.Sprintln(args...))
}

func (t *testingTB) Log(args ...any) {
	t.logs = append(t.logs, fmt.Sprintln(args...))
}

func (t *testingTB) Logf(format string, args ...any) {
	t.logs = append(t.logs, fmt.Sprintf(format, args...))
}

func TestFailures(t *testing.T) {
	checkFailure := func(f *Failure, errorMsg string) func(t *testing.T) {
		return func(t *testing.T) {
			t.Helper()
			fake := f.tb.(*testingTB)
			if len(fake.errors) != 1 {
				t.Errorf("expected one error, got %d: %v", len(fake.errors), fake.errors)
			} else if !strings.Contains(fake.errors[0], errorMsg) {
				t.Errorf("missing %q from error message: %v", errorMsg, fake.errors[0])
			}
			if fake.failNow {
				t.Errorf("failNow should not be set yet")
			}

			f.Log("should be logged").
				Logf("should be logged %s", "as well")
			if len(fake.logs) != 2 {
				t.Errorf("expected two logs, got %d: %v", len(fake.logs), fake.logs)
			}

			f.Fatal()
			if !fake.failNow {
				t.Errorf("failNow should now be set")
			}
		}
	}

	t.Run("True", checkFailure(True(&testingTB{}, false, "oups", "faux"), "oups faux"))

	t.Run("Equal", checkFailure(Equal(&testingTB{}, 1, 2), "Not equal:"))

	t.Run("EqualDeep", checkFailure(EqualDeep(&testingTB{}, map[string]int{"one": 2, "two": 2}, map[string]int{"one": 1, "two": 2}), "Not deep equal:"))
	t.Run("EqualDeep/nil/map", checkFailure(EqualDeep(&testingTB{}, nil, map[string]int{}), "Not deep equal:"))

	t.Run("EqualSlice/nil/0", checkFailure(EqualSlice(&testingTB{}, nil, []int{}), "Not expected emptiness"))
	t.Run("EqualSlice/0/nil", checkFailure(EqualSlice(&testingTB{}, []int{}, nil), "Not expected emptiness"))

	t.Run("EqualSlice/1/nil", checkFailure(EqualSlice(&testingTB{}, []int{1}, nil), "--- want(len=1) +++ got(len=0)"))
	t.Run("EqualSlice/nil/1", checkFailure(EqualSlice(&testingTB{}, nil, []int{1}), "Not expected emptiness"))

	t.Run("EqualSlice/1/1", checkFailure(EqualSlice(&testingTB{}, []int{1}, []int{2}), "--- want(len=1) +++ got(len=1)"))
	t.Run("EqualSlice/1/2", checkFailure(EqualSlice(&testingTB{}, []int{1}, []int{1, 2}), "--- want(len=1) +++ got(len=2)"))
	t.Run("EqualSlice/1/2", checkFailure(EqualSlice(&testingTB{}, []int{1}, []int{2, 1}), "--- want(len=1) +++ got(len=2)"))
	t.Run("EqualSlice/2/2", checkFailure(EqualSlice(&testingTB{}, []int{1, 2}, []int{2, 1}), "--- want(len=2) +++ got(len=2)"))
	t.Run("EqualSlice/2/2", checkFailure(EqualSlice(&testingTB{}, []int{1, 2}, []int{1, 1}), "--- want(len=2) +++ got(len=2)"))
	t.Run("EqualSlice/2/3", checkFailure(EqualSlice(&testingTB{}, []int{1, 2}, []int{2, 1, 3}), "--- want(len=2) +++ got(len=3)"))
	t.Run("EqualSlice/100x1/100x42", checkFailure(EqualSlice(&testingTB{}, bytes.Repeat([]byte{1}, 100), bytes.Repeat([]byte{1, 42}, 50)), "--- want(len=100) +++ got(len=100)"))
	t.Run("EqualSlice/100x1/101x42", checkFailure(EqualSlice(&testingTB{}, bytes.Repeat([]byte{1}, 100), bytes.Repeat([]byte{1, 42}, 51)), "--- want(len=100) +++ got(len=102)"))
}

func checkDiff[T comparable](t *testing.T, want, got []T, diff string) {
	t.Helper()

	tb := &testingTB{}
	EqualSlice(tb, want, got)
	Equal(t, 1, len(tb.errors)).Log(tb.errors).Fatal()
	Equal(t, strconv.Quote(diff), strconv.Quote(tb.errors[0])).Log("DIFF:", tb.errors[0])
}

func TestDiff(t *testing.T) {
	checkDiff(t, []int{1, 2, 3, 4, 5}, []int{5, 3, 4}, `--- want(len=5) +++ got(len=3)
@@ -1,5 +1,3 @@
-1
-2
+5
 3
 4
-5
`)

	checkDiff(t, []int{1, 2, 3, 4, 5}, []int{1, 2, 3, 5, 4}, `--- want(len=5) +++ got(len=5)
@@ -2,4 +2,4 @@
 2
 3
-4
 5
+4
`)

	checkDiff(t, []string{"1", " -"}, []string{"\r\nhello", "\"quotes\""}, `--- want(len=2) +++ got(len=2)
@@ -1,2 +1,2 @@
-1
- -
+\r\nhello
+"quotes"
`)

	type s struct {
		a, b int
	}
	checkDiff(t, []s{{1, 2}, {2, 3}}, []s{{1, 2}, {2, 4}}, `--- want(len=2) +++ got(len=2)
@@ -1,2 +1,2 @@
 check.s{a:1, b:2}
-check.s{a:2, b:3}
+check.s{a:2, b:4}
`)

	checkDiff(t, []int{1, 2, 3, 4, 5, 6}, []int{2, 3, 4, 5}, `--- want(len=6) +++ got(len=4)
@@ -1,6 +1,4 @@
-1
 2
 3
 4
 5
-6
`)

	checkDiff(t, []int{1, 2, 3, 4, 5, 6, 7}, []int{2, 3, 4, 5, 6}, `--- want(len=7) +++ got(len=5)
@@ -1,3 +1,2 @@
-1
 2
 3
@@ -5,3 +4,2 @@
 5
 6
-7
`)

	checkDiff(t, []int{1, 2, 3, 4, 5, 6, 7, 8}, []int{2, 3, 4, 5, 6, 8}, `--- want(len=8) +++ got(len=6)
@@ -1,3 +1,2 @@
-1
 2
 3
@@ -5,4 +4,3 @@
 5
 6
-7
 8
`)
}
