// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package synctest_test

import (
	"fmt"
	"internal/testenv"
	"os"
	"regexp"
	"testing"
	"testing/synctest"
	"time"
)

// Tests for interactions between synctest bubbles and the testing package.
// Other bubble behaviors are tested in internal/synctest.

func TestSuccess(t *testing.T) {
	synctest.Test(t, func(t *testing.T) {
	})
}

func TestFatal(t *testing.T) {
	runTest(t, nil, func() {
		synctest.Test(t, func(t *testing.T) {
			t.Fatal("fatal")
		})
	}, `^--- FAIL: TestFatal.*
    synctest_test.go:.* fatal
FAIL
$`)
}

func TestError(t *testing.T) {
	runTest(t, nil, func() {
		synctest.Test(t, func(t *testing.T) {
			t.Error("error")
		})
	}, `^--- FAIL: TestError.*
    synctest_test.go:.* error
FAIL
$`)
}

func TestVerboseError(t *testing.T) {
	runTest(t, []string{"-test.v"}, func() {
		synctest.Test(t, func(t *testing.T) {
			t.Error("error")
		})
	}, `^=== RUN   TestVerboseError
    synctest_test.go:.* error
--- FAIL: TestVerboseError.*
FAIL
$`)
}

func TestSkip(t *testing.T) {
	runTest(t, nil, func() {
		synctest.Test(t, func(t *testing.T) {
			t.Skip("skip")
		})
	}, `^PASS
$`)
}

func TestVerboseSkip(t *testing.T) {
	runTest(t, []string{"-test.v"}, func() {
		synctest.Test(t, func(t *testing.T) {
			t.Skip("skip")
		})
	}, `^=== RUN   TestVerboseSkip
    synctest_test.go:.* skip
--- PASS: TestVerboseSkip.*
PASS
$`)
}

func TestCleanup(t *testing.T) {
	done := false
	synctest.Test(t, func(t *testing.T) {
		ch := make(chan struct{})
		t.Cleanup(func() {
			// This cleanup function should execute inside the test's bubble.
			// (If it doesn't the runtime will panic.)
			close(ch)
		})
		// synctest.Test will wait for this goroutine to exit before returning.
		// The cleanup function signals the goroutine to exit before the wait starts.
		go func() {
			<-ch
			done = true
		}()
	})
	if !done {
		t.Fatalf("background goroutine did not return")
	}
}

func TestContext(t *testing.T) {
	state := "not started"
	synctest.Test(t, func(t *testing.T) {
		go func() {
			state = "waiting on context"
			<-t.Context().Done()
			state = "done"
		}()
		// Wait blocks until the goroutine above is blocked on t.Context().Done().
		synctest.Wait()
		if got, want := state, "waiting on context"; got != want {
			t.Fatalf("state = %q, want %q", got, want)
		}
	})
	// t.Context() is canceled before the test completes,
	// and synctest.Test does not return until the goroutine has set its state to "done".
	if got, want := state, "done"; got != want {
		t.Fatalf("state = %q, want %q", got, want)
	}
}

func TestDeadline(t *testing.T) {
	synctest.Test(t, func(t *testing.T) {
		defer wantPanic(t, "testing: t.Deadline called inside synctest bubble")
		_, _ = t.Deadline()
	})
}

func TestParallel(t *testing.T) {
	synctest.Test(t, func(t *testing.T) {
		defer wantPanic(t, "testing: t.Parallel called inside synctest bubble")
		t.Parallel()
	})
}

func TestRun(t *testing.T) {
	synctest.Test(t, func(t *testing.T) {
		defer wantPanic(t, "testing: t.Run called inside synctest bubble")
		t.Run("subtest", func(t *testing.T) {
		})
	})
}

func TestHelper(t *testing.T) {
	runTest(t, []string{"-test.v"}, func() {
		synctest.Test(t, func(t *testing.T) {
			helperLog(t, "log in helper")
		})
	}, `^=== RUN   TestHelper
    synctest_test.go:.* log in helper
--- PASS: TestHelper.*
PASS
$`)
}

func wantPanic(t *testing.T, want string) {
	if e := recover(); e != nil {
		if got := fmt.Sprint(e); got != want {
			t.Errorf("got panic message %q, want %q", got, want)
		}
	} else {
		t.Errorf("got no panic, want one")
	}
}

func runTest(t *testing.T, args []string, f func(), pattern string) {
	if os.Getenv("GO_WANT_HELPER_PROCESS") == "1" {
		f()
		return
	}
	t.Helper()
	re := regexp.MustCompile(pattern)
	testenv.MustHaveExec(t)
	cmd := testenv.Command(t, testenv.Executable(t), "-test.run=^"+regexp.QuoteMeta(t.Name())+"$", "-test.count=1")
	cmd.Args = append(cmd.Args, args...)
	cmd = testenv.CleanCmdEnv(cmd)
	cmd.Env = append(cmd.Env, "GO_WANT_HELPER_PROCESS=1")
	out, _ := cmd.CombinedOutput()
	if !re.Match(out) {
		t.Errorf("got output:\n%s\nwant matching:\n%s", out, pattern)
	}
}

func TestNow(t *testing.T) {
	synctest.Test(t, func(t *testing.T) {
		if got, want := time.Now(), time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC); !got.Equal(want) {
			t.Errorf("time.Now() = %v, want %v", got, want)
		}
	})
}

func TestSynctestTimerRaceCtxCrash(t *testing.T) {
	for range 20 {
		t.Run("", func(t *testing.T) {
			synctest.Test(t, func(t *testing.T) {
				for range 100 {
					time.AfterFunc(1*time.Second, func() {
						time.AfterFunc(0, func() {})
					})
				}
				time.Sleep(2 * time.Second)
			})
		})
	}
}
