// Copyright 2026 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 sql

import (
	"runtime"
	"testing"
	"testing/synctest"
	"time"
)

func TestClosingMutex(t *testing.T) {
	start := func(t *testing.T, f func()) func() bool {
		done := false
		go func() {
			f()
			done = true
		}()
		return func() bool {
			synctest.Wait()
			return done
		}
	}

	synctest.Test(t, func(t *testing.T) {
		var m closingMutex

		// RLock does not block RLock.
		m.RLock()
		m.RLock()
		m.RUnlock()
		m.RUnlock()

		// RLock blocks Lock.
		m.RLock()
		lock1Done := start(t, m.Lock)
		if lock1Done() {
			t.Fatalf("m.Lock(): succeeded on RLocked mutex")
		}
		m.RLock()
		m.RUnlock()
		if lock1Done() {
			t.Fatalf("m.Lock(): succeeded after one RUnlock, one RLock remains")
		}
		m.RUnlock()
		if !lock1Done() {
			t.Fatalf("m.Lock(): still blocking after all RUnlocks")
		}
		m.Unlock()

		// Lock blocks RLock.
		m.Lock()
		rlock1Done := start(t, m.RLock)
		rlock2Done := start(t, m.RLock)
		if rlock1Done() || rlock2Done() {
			t.Fatalf("m.RLock(): succeeded on Locked mutex")
		}
		m.Unlock()
		if !rlock1Done() || !rlock2Done() {
			t.Fatalf("m.RLock(): succeeded on Locked mutex")
		}
		m.RUnlock()
		m.RUnlock()

		// Lock blocks Lock.
		m.Lock()
		lock2Done := start(t, m.Lock)
		if lock2Done() {
			t.Fatalf("m.Lock(): succeeded on Locked mutex")
		}
		m.Unlock()
		if !lock2Done() {
			t.Fatalf("m.Lock(): still blocking after Unlock")
		}
		m.Unlock()

		// Lock on RLocked mutex does not block RLock.
		m.RLock()
		lock3Done := start(t, m.Lock)
		if lock3Done() {
			t.Fatalf("m.Lock(): succeeded on RLocked mutex")
		}
		m.RLock()
		m.RUnlock()
		m.RUnlock()
		if !lock3Done() {
			t.Fatalf("m.Lock(): still blocking after RUnlock")
		}
		m.Unlock()
	})
}

func TestClosingMutexLockStarvation(t *testing.T) {
	synctest.Test(t, func(t *testing.T) {
		// Run this test for a few iterations, to avoid racy successes.
		for range 100 {
			var m closingMutex

			// With the mutex RLocked, try to Lock it. Lock blocks.
			m.RLock()
			locked := false
			go func() {
				m.Lock()
				locked = true
				m.Unlock()
			}()
			synctest.Wait()
			if locked {
				t.Errorf("lock acquired while mutex is rlocked")
			}

			// Add and drop another RLock. Lock is still blocking.
			m.RLock()
			m.RUnlock()
			if locked {
				t.Errorf("lock acquired while mutex is double-rlocked")
			}

			// Drop and reacquire the RLock.
			// The blocking Lock should always acquire the mutex
			// before the RLock succeeds.
			m.RUnlock()
			m.RLock()
			if !locked {
				t.Errorf("lock not acquired when rlock dropped")
			}
			m.RUnlock()
		}
	})
}

func TestClosingMutexLockRLockRace(t *testing.T) {
	oldProcs := runtime.GOMAXPROCS(2)
	defer runtime.GOMAXPROCS(oldProcs)

	// Race Lock against readers entering and leaving. A stale state in Lock
	// can leave the mutex in state 1 with no reader remaining to wake the
	// writer, at which point the writer will never finish.
	var m closingMutex
	done := make(chan struct{})
	go func() {
		defer close(done)
		for range 10_000 {
			m.Lock()
			m.Unlock()
		}
	}()

	// Yield after each reader attempt so the writer can run on targets with
	// one P and no goroutine preemption, such as wasm.
	for range 100_000 {
		select {
		case <-done:
			return
		default:
		}
		if m.TryRLock() {
			m.RUnlock()
		}
		runtime.Gosched()
	}
	select {
	case <-done:
	case <-time.After(time.Minute):
		if state := m.state.Load(); state == 1 {
			t.Fatalf("Lock left the mutex in state 1 with no reader to wake the writer")
		}
		t.Fatalf("Lock did not complete within one minute; mutex state is %v", m.state.Load())
	}
}

func TestClosingMutexPanics(t *testing.T) {
	for _, test := range []struct {
		name string
		f    func()
	}{{
		name: "double RUnlock",
		f: func() {
			var m closingMutex
			m.RLock()
			m.RUnlock()
			m.RUnlock()
		},
	}, {
		name: "double Unlock",
		f: func() {
			var m closingMutex
			m.Lock()
			m.Unlock()
			m.Unlock()
		},
	}} {
		var got any
		func() {
			defer func() {
				got = recover()
			}()
			test.f()
		}()
		if got == nil {
			t.Errorf("no panic, want one")
		}
	}
}
