// Package utils provides utility functions for the AutoSSO application.
// This file contains tests for panic recovery utilities.
//
// Requirements: 6.6
package utils

import (
	"strings"
	"sync"
	"testing"
	"time"
)

// TestRecoverWithHandler tests that RecoverWithHandler catches panics and calls the handler.
func TestRecoverWithHandler(t *testing.T) {
	var handlerCalled bool
	var capturedInfo *PanicInfo

	func() {
		defer RecoverWithHandler("test-goroutine", func(info *PanicInfo) {
			handlerCalled = true
			capturedInfo = info
		})
		panic("test panic")
	}()

	if !handlerCalled {
		t.Error("handler was not called")
	}

	if capturedInfo == nil {
		t.Fatal("panic info was nil")
	}

	if capturedInfo.Value != "test panic" {
		t.Errorf("expected panic value 'test panic', got %v", capturedInfo.Value)
	}

	if capturedInfo.GoroutineName != "test-goroutine" {
		t.Errorf("expected goroutine name 'test-goroutine', got %s", capturedInfo.GoroutineName)
	}

	if capturedInfo.Stack == "" {
		t.Error("stack trace was empty")
	}
}

// TestRecoverWithHandlerNoPanic tests that RecoverWithHandler doesn't call handler when no panic.
func TestRecoverWithHandlerNoPanic(t *testing.T) {
	var handlerCalled bool

	func() {
		defer RecoverWithHandler("test-goroutine", func(info *PanicInfo) {
			handlerCalled = true
		})
		// No panic
	}()

	if handlerCalled {
		t.Error("handler was called when no panic occurred")
	}
}

// TestRecoverWithHandlerNilHandler tests that RecoverWithHandler works with nil handler.
func TestRecoverWithHandlerNilHandler(t *testing.T) {
	// This should not panic even with nil handler
	func() {
		defer RecoverWithHandler("test-goroutine", nil)
		panic("test panic")
	}()
	// If we get here, the test passed
}

// TestRecover tests the Recover function.
// Note: Recover must be called directly in a deferred function that also calls recover().
// This test verifies the PanicInfo structure creation.
func TestRecover(t *testing.T) {
	// Test that RecoverWithHandler properly creates PanicInfo
	var info *PanicInfo

	func() {
		defer RecoverWithHandler("test-goroutine", func(pi *PanicInfo) {
			info = pi
		})
		panic("test panic")
	}()

	if info == nil {
		t.Fatal("panic info was nil")
	}

	if info.Value != "test panic" {
		t.Errorf("expected panic value 'test panic', got %v", info.Value)
	}

	if info.GoroutineName != "test-goroutine" {
		t.Errorf("expected goroutine name 'test-goroutine', got %s", info.GoroutineName)
	}
}

// TestRecoverNoPanic tests that RecoverWithHandler doesn't call handler when no panic.
func TestRecoverNoPanic(t *testing.T) {
	var handlerCalled bool

	func() {
		defer RecoverWithHandler("test-goroutine", func(info *PanicInfo) {
			handlerCalled = true
		})
		// No panic
	}()

	if handlerCalled {
		t.Error("expected handler not to be called when no panic occurred")
	}
}

// TestSafeGo tests that SafeGo runs the function and recovers from panics.
func TestSafeGo(t *testing.T) {
	var handlerCalled bool
	var capturedInfo *PanicInfo
	var mu sync.Mutex
	done := make(chan struct{})

	SafeGo("test-worker", func() {
		panic("worker panic")
	}, func(info *PanicInfo) {
		mu.Lock()
		handlerCalled = true
		capturedInfo = info
		mu.Unlock()
		close(done)
	})

	// Wait for handler to be called
	select {
	case <-done:
		// OK
	case <-time.After(time.Second):
		t.Fatal("timeout waiting for panic handler")
	}

	mu.Lock()
	defer mu.Unlock()

	if !handlerCalled {
		t.Error("handler was not called")
	}

	if capturedInfo == nil {
		t.Fatal("panic info was nil")
	}

	if capturedInfo.Value != "worker panic" {
		t.Errorf("expected panic value 'worker panic', got %v", capturedInfo.Value)
	}

	if capturedInfo.GoroutineName != "test-worker" {
		t.Errorf("expected goroutine name 'test-worker', got %s", capturedInfo.GoroutineName)
	}
}

// TestSafeGoNoPanic tests that SafeGo works normally when no panic.
func TestSafeGoNoPanic(t *testing.T) {
	var wg sync.WaitGroup
	var executed bool
	var handlerCalled bool
	var mu sync.Mutex

	wg.Add(1)
	SafeGo("test-worker", func() {
		mu.Lock()
		executed = true
		mu.Unlock()
		wg.Done()
	}, func(info *PanicInfo) {
		mu.Lock()
		handlerCalled = true
		mu.Unlock()
	})

	// Wait for goroutine to complete
	done := make(chan struct{})
	go func() {
		wg.Wait()
		close(done)
	}()

	select {
	case <-done:
		// OK
	case <-time.After(time.Second):
		t.Fatal("timeout waiting for goroutine")
	}

	mu.Lock()
	defer mu.Unlock()

	if !executed {
		t.Error("function was not executed")
	}

	if handlerCalled {
		t.Error("handler was called when no panic occurred")
	}
}

// TestMustNotPanic tests the MustNotPanic function.
func TestMustNotPanic(t *testing.T) {
	// Test with panic
	err := MustNotPanic(func() {
		panic("test panic")
	})

	if err == nil {
		t.Error("expected error when function panics")
	}

	if !strings.Contains(err.Error(), "test panic") {
		t.Errorf("error should contain panic value, got: %v", err)
	}

	if !strings.Contains(err.Error(), "panic recovered") {
		t.Errorf("error should contain 'panic recovered', got: %v", err)
	}

	// Test without panic
	err = MustNotPanic(func() {
		// No panic
	})

	if err != nil {
		t.Errorf("expected nil error when no panic, got: %v", err)
	}
}

// TestWrapPanic tests the WrapPanic function.
func TestWrapPanic(t *testing.T) {
	// Test with panic
	err := WrapPanic(func() error {
		panic("test panic")
	})

	if err == nil {
		t.Error("expected error when function panics")
	}

	if !strings.Contains(err.Error(), "test panic") {
		t.Errorf("error should contain panic value, got: %v", err)
	}

	// Test without panic, returning nil
	err = WrapPanic(func() error {
		return nil
	})

	if err != nil {
		t.Errorf("expected nil error when no panic and function returns nil, got: %v", err)
	}

	// Test without panic, returning error
	expectedErr := "expected error"
	err = WrapPanic(func() error {
		return &testError{msg: expectedErr}
	})

	if err == nil {
		t.Error("expected error when function returns error")
	}

	if err.Error() != expectedErr {
		t.Errorf("expected error '%s', got: %v", expectedErr, err)
	}
}

// testError is a simple error type for testing.
type testError struct {
	msg string
}

func (e *testError) Error() string {
	return e.msg
}

// TestGetStackTrace tests the GetStackTrace function.
func TestGetStackTrace(t *testing.T) {
	stack := GetStackTrace(0)

	if stack == "" {
		t.Error("stack trace was empty")
	}

	// Stack trace should contain goroutine header
	if !strings.Contains(stack, "goroutine") {
		t.Error("stack trace should contain 'goroutine'")
	}

	// Stack trace should contain this test function
	if !strings.Contains(stack, "TestGetStackTrace") {
		t.Error("stack trace should contain 'TestGetStackTrace'")
	}
}

// TestPanicInfoString tests the PanicInfo.String method.
func TestPanicInfoString(t *testing.T) {
	info := &PanicInfo{
		Value:         "test panic",
		GoroutineName: "test-worker",
		Stack:         "goroutine 1 [running]:\nmain.main()\n\t/path/to/file.go:10",
	}

	str := info.String()

	if !strings.Contains(str, "test panic") {
		t.Error("string should contain panic value")
	}

	if !strings.Contains(str, "test-worker") {
		t.Error("string should contain goroutine name")
	}

	if !strings.Contains(str, "goroutine 1") {
		t.Error("string should contain stack trace")
	}
}

// TestPanicInfoStringNoName tests PanicInfo.String without goroutine name.
func TestPanicInfoStringNoName(t *testing.T) {
	info := &PanicInfo{
		Value: "test panic",
		Stack: "goroutine 1 [running]:",
	}

	str := info.String()

	if !strings.Contains(str, "test panic") {
		t.Error("string should contain panic value")
	}

	// Should not contain "(goroutine: )" when name is empty
	if strings.Contains(str, "(goroutine: )") {
		t.Error("string should not contain empty goroutine name")
	}
}

// TestPanicInfoStringNoStack tests PanicInfo.String without stack trace.
func TestPanicInfoStringNoStack(t *testing.T) {
	info := &PanicInfo{
		Value:         "test panic",
		GoroutineName: "test-worker",
	}

	str := info.String()

	if !strings.Contains(str, "test panic") {
		t.Error("string should contain panic value")
	}

	if !strings.Contains(str, "test-worker") {
		t.Error("string should contain goroutine name")
	}
}

// TestConcurrentPanicRecovery tests that panic recovery works correctly under concurrent conditions.
func TestConcurrentPanicRecovery(t *testing.T) {
	const numGoroutines = 10
	var wg sync.WaitGroup
	var mu sync.Mutex
	recoveredCount := 0

	for i := 0; i < numGoroutines; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			defer RecoverWithHandler("concurrent-worker", func(info *PanicInfo) {
				mu.Lock()
				recoveredCount++
				mu.Unlock()
			})
			// Every goroutine panics
			panic("concurrent panic")
		}(i)
	}

	wg.Wait()

	mu.Lock()
	defer mu.Unlock()

	if recoveredCount != numGoroutines {
		t.Errorf("expected %d recovered panics, got %d", numGoroutines, recoveredCount)
	}
}

// TestPanicWithDifferentTypes tests panic recovery with different panic value types.
func TestPanicWithDifferentTypes(t *testing.T) {
	testCases := []struct {
		name       string
		panicValue interface{}
		checkValue func(interface{}) bool
	}{
		{"string", "string panic", func(v interface{}) bool { return v == "string panic" }},
		{"int", 42, func(v interface{}) bool { return v == 42 }},
		{"error", &testError{msg: "error panic"}, func(v interface{}) bool {
			e, ok := v.(*testError)
			return ok && e.msg == "error panic"
		}},
		{"struct", struct{ msg string }{"struct panic"}, func(v interface{}) bool {
			s, ok := v.(struct{ msg string })
			return ok && s.msg == "struct panic"
		}},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			var capturedInfo *PanicInfo

			func() {
				defer RecoverWithHandler("test", func(info *PanicInfo) {
					capturedInfo = info
				})
				panic(tc.panicValue)
			}()

			if capturedInfo == nil {
				t.Fatal("panic info was nil")
			}

			if !tc.checkValue(capturedInfo.Value) {
				t.Errorf("unexpected panic value: %v", capturedInfo.Value)
			}
		})
	}
}
