package fiber

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"io"
	stdlog "log" //nolint:depguard // Test needs the concrete stdlib logger type to restore the previous writer.
	"strings"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	"github.com/gofiber/fiber/v3/log"
	"github.com/stretchr/testify/require"
)

const (
	terminateErrorMessage = "terminate error"
	startErrorMessage     = "start error"
)

// mockService implements Service interface for testing
type mockService struct {
	startError     error
	terminateError error
	stateError     error
	name           string
	started        bool
	terminated     bool
	startDelay     time.Duration
	terminateDelay time.Duration
}

type shutdownHookStorage struct {
	closeErr    error
	closeCalled atomic.Bool
}

type stringsLogger struct {
	strings.Builder
}

var servicesTestLogOutputMu sync.Mutex

func withCapturedLogOutput(t *testing.T, writer io.Writer) {
	t.Helper()

	servicesTestLogOutputMu.Lock()
	cleanupRegistered := false
	defer func() {
		if !cleanupRegistered {
			servicesTestLogOutputMu.Unlock()
		}
	}()

	currentOutput := log.DefaultLogger[*stdlog.Logger]().Logger().Writer()
	t.Cleanup(func() {
		log.SetOutput(currentOutput)
		servicesTestLogOutputMu.Unlock()
	})
	cleanupRegistered = true

	log.SetOutput(writer)
}

func (*shutdownHookStorage) GetWithContext(context.Context, string) ([]byte, error) {
	return nil, nil
}

func (*shutdownHookStorage) Get(string) ([]byte, error) {
	return nil, nil
}

func (*shutdownHookStorage) SetWithContext(context.Context, string, []byte, time.Duration) error {
	return nil
}

func (*shutdownHookStorage) Set(string, []byte, time.Duration) error {
	return nil
}

func (*shutdownHookStorage) DeleteWithContext(context.Context, string) error {
	return nil
}

func (*shutdownHookStorage) Delete(string) error {
	return nil
}

func (*shutdownHookStorage) ResetWithContext(context.Context) error {
	return nil
}

func (*shutdownHookStorage) Reset() error {
	return nil
}

func (s *shutdownHookStorage) Close() error {
	s.closeCalled.Store(true)
	return s.closeErr
}

func (m *mockService) Start(ctx context.Context) error {
	select {
	case <-ctx.Done():
		return fmt.Errorf("context canceled: %w", ctx.Err())
	default:
	}

	if m.startDelay > 0 {
		timer := time.NewTimer(m.startDelay)
		select {
		case <-ctx.Done():
			timer.Stop()
			return fmt.Errorf("context canceled: %w", ctx.Err())
		case <-timer.C:
			// Continue after delay
		}
	}

	if m.startError != nil {
		m.started = false
		return m.startError
	}

	m.started = true
	return nil
}

func (m *mockService) String() string {
	return m.name
}

func (m *mockService) State(ctx context.Context) (string, error) {
	if ctx.Err() != nil {
		return "", fmt.Errorf("context canceled: %w", ctx.Err())
	}

	if m.stateError != nil {
		return "error", m.stateError
	}

	if m.started {
		return "running", nil
	}

	if m.terminated {
		return "stopped", nil
	}

	return "unknown", nil
}

func (m *mockService) Terminate(ctx context.Context) error {
	select {
	case <-ctx.Done():
		return fmt.Errorf("context canceled: %w", ctx.Err())
	default:
	}

	if m.terminateDelay > 0 {
		timer := time.NewTimer(m.terminateDelay)
		select {
		case <-ctx.Done():
			timer.Stop()
			return fmt.Errorf("context canceled: %w", ctx.Err())
		case <-timer.C:
			// Continue after delay
		}
	}

	if m.terminateError != nil {
		m.terminated = false
		return m.terminateError
	}

	m.started = false
	m.terminated = true
	return nil
}

func Test_HasConfiguredServices(t *testing.T) {
	testHasConfiguredServicesFn := func(t *testing.T, app *App, expected bool) {
		t.Helper()

		result := app.hasConfiguredServices()
		require.Equal(t, expected, result)
	}

	t.Run("no-services", func(t *testing.T) {
		testHasConfiguredServicesFn(t, &App{configured: Config{}}, false)
	})

	t.Run("has-services", func(t *testing.T) {
		testHasConfiguredServicesFn(t, &App{configured: Config{Services: []Service{&mockService{name: "test-dep"}}}}, true)
	})
}

func Test_InitServices(t *testing.T) {
	t.Parallel()

	t.Run("no-services", func(t *testing.T) {
		app := &App{configured: Config{}}
		require.NotPanics(t, app.initServices)
	})

	t.Run("start/success", func(t *testing.T) {
		// Initialize the app using the struct and defining the state and hooks manually,
		// because we are not checking the shutdown hooks in this test.
		app := &App{
			configured: Config{
				Services: []Service{
					&mockService{name: "dep1"},
					&mockService{name: "dep2"},
				},
			},
			state: newState(),
		}

		app.hooks = newHooks(app)

		require.NotPanics(t, app.initServices)
	})

	t.Run("start/error", func(t *testing.T) {
		// Initialize the app using the struct and defining the state and hooks manually,
		// because we are not checking the shutdown hooks in this test.
		app := &App{
			configured: Config{
				Services: []Service{
					&mockService{name: "dep1", startError: errors.New(startErrorMessage + " 1")},
					&mockService{name: "dep2", startError: errors.New(startErrorMessage + " 2")},
					&mockService{name: "dep3"},
				},
			},
			state: newState(),
		}

		app.hooks = newHooks(app)

		require.Panics(t, app.initServices)
	})

	t.Run("shutdown-hooks/success", func(t *testing.T) {
		// Initialize the app using the New function to verify that the shutdown hooks are registered
		// and the app mutex is not causing a deadlock.
		app := New(Config{
			Services: []Service{&mockService{name: "dep1"}},
		})

		require.NotPanics(t, app.initServices)

		var buf stringsLogger
		log.SetOutput(&buf)

		app.Hooks().executeOnPostShutdownHooks(nil)

		require.NotContains(t, buf.String(), "failed to call post shutdown hook:")
	})

	t.Run("shutdown-hooks/error", func(t *testing.T) {
		// Initialize the app using the New function to verify that the shutdown hooks are registered
		// and the app mutex is not causing a deadlock.
		app := New(Config{
			Services: []Service{
				&mockService{name: "dep1"},
				&mockService{name: "dep2", terminateError: errors.New(terminateErrorMessage + " 2")},
			},
		})

		require.NotPanics(t, app.initServices)

		var buf stringsLogger
		log.SetOutput(&buf)

		app.Hooks().executeOnPostShutdownHooks(nil)

		require.Contains(t, buf.String(), "failed to shutdown services: service dep2 terminate: terminate error 2")
	})

	t.Run("shutdown-hooks/close-shared-state", func(t *testing.T) {
		t.Parallel()

		storage := &shutdownHookStorage{}
		app := New(Config{
			Services:      []Service{&mockService{name: "dep1"}},
			SharedStorage: storage,
		})

		require.NotPanics(t, app.initServices)

		var buf stringsLogger
		withCapturedLogOutput(t, &buf)

		app.Hooks().executeOnPostShutdownHooks(nil)

		require.True(t, storage.closeCalled.Load())
		require.NotContains(t, buf.String(), "failed to close sharedState:")
	})

	t.Run("shutdown-hooks/close-shared-state-after-service-error", func(t *testing.T) {
		t.Parallel()

		storage := &shutdownHookStorage{closeErr: errors.New("close error")}
		app := New(Config{
			Services: []Service{
				&mockService{name: "dep1"},
				&mockService{name: "dep2", terminateError: errors.New(terminateErrorMessage + " 2")},
			},
			SharedStorage: storage,
		})

		require.NotPanics(t, app.initServices)

		var buf stringsLogger
		withCapturedLogOutput(t, &buf)

		app.Hooks().executeOnPostShutdownHooks(nil)

		require.True(t, storage.closeCalled.Load())
		require.Contains(t, buf.String(), "failed to shutdown services: service dep2 terminate: terminate error 2")
		require.Contains(t, buf.String(), "failed to close sharedState: close error")
	})
}

func Test_StartServices(t *testing.T) {
	t.Run("no-services", func(t *testing.T) {
		app := &App{
			configured: Config{
				Services: []Service{},
			},
			state: newState(),
		}

		err := app.startServices(context.Background())
		require.NoError(t, err)
		require.Zero(t, app.state.ServicesLen())
	})

	t.Run("successful-start", func(t *testing.T) {
		app := &App{
			configured: Config{
				Services: []Service{
					&mockService{name: "dep1"},
					&mockService{name: "dep2"},
				},
			},
			state: newState(),
		}

		err := app.startServices(context.Background())
		require.NoError(t, err)
		require.Equal(t, 2, app.state.ServicesLen())
	})

	t.Run("failed-start", func(t *testing.T) {
		app := &App{
			configured: Config{
				Services: []Service{
					&mockService{name: "dep1", startError: errors.New(startErrorMessage + " 1")},
					&mockService{name: "dep2", startError: errors.New(startErrorMessage + " 2")},
					&mockService{name: "dep3"},
				},
			},
			state: newState(),
		}

		err := app.startServices(context.Background())
		require.Error(t, err)
		require.Contains(t, err.Error(), startErrorMessage+" 1")
		require.Contains(t, err.Error(), startErrorMessage+" 2")
		require.Equal(t, 1, app.state.ServicesLen())
	})

	t.Run("context", func(t *testing.T) {
		t.Run("already-canceled", func(t *testing.T) {
			app := &App{
				configured: Config{
					Services: []Service{
						&mockService{name: "dep1"},
					},
				},
				state: newState(),
			}

			// Create a context that is already canceled
			ctx, cancel := context.WithCancel(context.Background())
			cancel()

			err := app.startServices(ctx)
			require.ErrorIs(t, err, context.Canceled)
			require.Zero(t, app.state.ServicesLen())
		})

		t.Run("cancellation", func(t *testing.T) {
			// Create a service that takes some time to start
			slowDep := &mockService{name: "slow-dep", startDelay: 200 * time.Millisecond}

			app := &App{
				configured: Config{
					Services: []Service{slowDep},
				},
				state: newState(),
			}

			// Create a context that will be canceled immediately
			ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
			defer cancel()

			// Start services with a delay that is longer than the timeout
			err := app.startServices(ctx)
			require.ErrorIs(t, err, context.DeadlineExceeded)
			require.Zero(t, app.state.ServicesLen())
		})
	})
}

func Test_ShutdownServices(t *testing.T) {
	t.Run("no-services", func(t *testing.T) {
		app := &App{
			configured: Config{
				Services: []Service{},
			},
			state: newState(),
		}

		err := app.shutdownServices(context.Background())
		require.NoError(t, err)
		require.Zero(t, app.state.ServicesLen())
	})

	t.Run("successful-shutdown", func(t *testing.T) {
		srv1 := &mockService{name: "dep1"}
		srv2 := &mockService{name: "dep2"}

		// Expected state, including the two started services
		expectedState := newState()
		expectedState.setService(srv1)
		expectedState.setService(srv2)

		app := &App{
			configured: Config{
				Services: []Service{srv1, srv2},
			},
			state: expectedState,
		}

		err := app.shutdownServices(context.Background())
		require.NoError(t, err)
		require.Zero(t, app.state.ServicesLen())
	})

	t.Run("failed-shutdown", func(t *testing.T) {
		srv1 := &mockService{name: "dep1", terminateError: errors.New(terminateErrorMessage + " 1")}
		srv2 := &mockService{name: "dep2", terminateError: errors.New(terminateErrorMessage + " 2")}
		srv3 := &mockService{name: "dep3"}

		// Expected state, including the two started services
		expectedState := newState()
		expectedState.setService(srv1)
		expectedState.setService(srv2)
		expectedState.setService(srv3)

		app := &App{
			configured: Config{
				Services: []Service{srv1, srv2, srv3},
			},
			state: expectedState,
		}

		err := app.shutdownServices(context.Background())
		require.Error(t, err)
		require.Contains(t, err.Error(), terminateErrorMessage+" 1")
		require.Contains(t, err.Error(), terminateErrorMessage+" 2")
		require.Equal(t, 2, app.state.ServicesLen()) // 2 services failed to terminate
	})

	t.Run("context", func(t *testing.T) {
		t.Run("already-canceled", func(t *testing.T) {
			srv1 := &mockService{name: "dep1"}

			// Expected state, including the two started services
			expectedState := newState()
			expectedState.setService(srv1)

			app := &App{
				configured: Config{
					Services: []Service{srv1},
				},
				state: expectedState,
			}

			// Create a context that is already canceled
			ctx, cancel := context.WithCancel(context.Background())
			cancel()

			err := app.shutdownServices(ctx)
			require.Error(t, err)
			require.ErrorIs(t, err, context.Canceled)
			require.Contains(t, err.Error(), "service dep1 terminate")
			require.Equal(t, 1, app.state.ServicesLen())
		})

		t.Run("cancellation", func(t *testing.T) {
			// Create a service that takes some time to terminate
			slowDep := &mockService{name: "slow-dep", terminateDelay: 200 * time.Millisecond}

			// Expected state, including the two started services
			expectedState := newState()
			expectedState.setService(slowDep)

			app := &App{
				configured: Config{
					Services: []Service{slowDep},
				},
				state: expectedState,
			}

			// Create a new context for shutdown
			ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
			defer cancel()

			// Shutdown services with canceled context
			err := app.shutdownServices(ctx)
			require.ErrorIs(t, err, context.DeadlineExceeded)
			require.Equal(t, 1, app.state.ServicesLen())
		})
	})
}

func Test_LogServices(t *testing.T) {
	t.Parallel()
	// Service with successful State
	runningService := &mockService{name: "running", started: true}
	// Service with State error
	errorService := &mockService{name: "error", stateError: errors.New("state error")}

	expectedState := newState()
	expectedState.setService(runningService)
	expectedState.setService(errorService)

	app := &App{
		configured: Config{
			Services: []Service{runningService, errorService},
		},
		state: expectedState,
	}

	var buf bytes.Buffer

	colors := Colors{
		Green: "\033[32m",
		Reset: "\033[0m",
		Blue:  "\033[34m",
		Red:   "\033[31m",
	}

	err := app.logServices(context.Background(), &buf, &colors)
	require.NoError(t, err)

	output := buf.String()

	for _, srv := range app.state.Services() {
		stateColor := colors.Blue
		state := "RUNNING"
		if _, err := srv.State(context.Background()); err != nil {
			stateColor = colors.Red
			state = "ERROR"
		}

		expected := fmt.Sprintf("%sINFO%s    🧩 %s[ %s ] %s%s\n", colors.Green, colors.Reset, stateColor, strings.ToUpper(state), srv.String(), colors.Reset)
		require.Contains(t, output, expected)
	}
}

func Test_NewConfiguredServicesNil(t *testing.T) {
	t.Parallel()

	require.PanicsWithError(t, "fiber: service at index 0 is nil", func() {
		New(Config{
			Services: []Service{nil},
		})
	})
}

func Test_ServiceContextProviders(t *testing.T) {
	t.Run("no-provider", func(t *testing.T) {
		app := &App{
			configured: Config{},
			state:      newState(),
		}
		require.Equal(t, context.Background(), app.servicesStartupCtx())
		require.Equal(t, context.Background(), app.servicesShutdownCtx())
	})

	t.Run("with-provider", func(t *testing.T) {
		ctx := context.TODO()
		app := &App{
			configured: Config{
				ServicesStartupContextProvider: func() context.Context {
					return ctx
				},
				ServicesShutdownContextProvider: func() context.Context {
					return ctx
				},
			},
			state: newState(),
		}

		require.Equal(t, ctx, app.servicesStartupCtx())
		require.Equal(t, ctx, app.servicesShutdownCtx())
	})
}

func Benchmark_StartServices(b *testing.B) {
	benchmarkFn := func(b *testing.B, services []Service) {
		b.Helper()

		for b.Loop() {
			app := New(Config{
				Services: services,
			})

			ctx := context.Background()
			if err := app.startServices(ctx); err != nil {
				b.Fatal("Expected no error but got", err)
			}
		}
	}

	b.Run("no-services", func(b *testing.B) {
		benchmarkFn(b, []Service{})
	})

	b.Run("single-service", func(b *testing.B) {
		benchmarkFn(b, []Service{
			&mockService{name: "dep1"},
		})
	})

	b.Run("multiple-services", func(b *testing.B) {
		benchmarkFn(b, []Service{
			&mockService{name: "dep1"},
			&mockService{name: "dep2"},
			&mockService{name: "dep3"},
		})
	})

	b.Run("multiple-services-with-delays", func(b *testing.B) {
		benchmarkFn(b, []Service{
			&mockService{name: "dep1", startDelay: 1 * time.Millisecond},
			&mockService{name: "dep2", startDelay: 2 * time.Millisecond},
			&mockService{name: "dep3", startDelay: 3 * time.Millisecond},
		})
	})
}

func Benchmark_ShutdownServices(b *testing.B) {
	benchmarkFn := func(b *testing.B, services []Service) {
		b.Helper()

		for b.Loop() {
			app := New(Config{
				Services: services,
			})

			ctx := context.Background()
			if err := app.shutdownServices(ctx); err != nil {
				b.Fatal("Expected no error but got", err)
			}
		}
	}

	b.Run("no-services", func(b *testing.B) {
		benchmarkFn(b, []Service{})
	})

	b.Run("single-service", func(b *testing.B) {
		benchmarkFn(b, []Service{
			&mockService{name: "dep1"},
		})
	})

	b.Run("multiple-services", func(b *testing.B) {
		benchmarkFn(b, []Service{
			&mockService{name: "dep1"},
			&mockService{name: "dep2"},
			&mockService{name: "dep3"},
		})
	})

	b.Run("multiple-services-with-delays", func(b *testing.B) {
		benchmarkFn(b, []Service{
			&mockService{name: "dep1", terminateDelay: 1 * time.Millisecond},
			&mockService{name: "dep2", terminateDelay: 2 * time.Millisecond},
			&mockService{name: "dep3", terminateDelay: 3 * time.Millisecond},
		})
	})
}

func Benchmark_StartServices_withContextCancellation(b *testing.B) {
	benchmarkFn := func(b *testing.B, services []Service, timeout time.Duration) {
		b.Helper()

		for b.Loop() {
			app := New(Config{
				Services: services,
			})

			ctx, cancel := context.WithTimeout(context.Background(), timeout)
			err := app.startServices(ctx)
			// We expect an error here due to the short timeout
			if err == nil && timeout < time.Second {
				b.Fatal("Expected error due to context cancellation but got none")
			}
			cancel()
		}
	}

	b.Run("single-service/immediate-cancellation", func(b *testing.B) {
		benchmarkFn(b, []Service{
			&mockService{name: "dep1", startDelay: 100 * time.Millisecond},
		}, 10*time.Millisecond)
	})

	b.Run("multiple-services/immediate-cancellation", func(b *testing.B) {
		benchmarkFn(b, []Service{
			&mockService{name: "dep1", startDelay: 100 * time.Millisecond},
			&mockService{name: "dep2", startDelay: 200 * time.Millisecond},
			&mockService{name: "dep3", startDelay: 300 * time.Millisecond},
		}, 10*time.Millisecond)
	})

	b.Run("multiple-services/successful-completion", func(b *testing.B) {
		const timeout = 500 * time.Millisecond

		for b.Loop() {
			app := New(Config{
				Services: []Service{
					&mockService{name: "dep1", startDelay: 10 * time.Millisecond},
					&mockService{name: "dep2", startDelay: 20 * time.Millisecond},
					&mockService{name: "dep3", startDelay: 30 * time.Millisecond},
				},
			})

			ctx, cancel := context.WithTimeout(context.Background(), timeout)
			err := app.startServices(ctx)
			if err != nil {
				b.Fatal("Expected no error but got", err)
			}
			cancel()
		}
	})
}

func Benchmark_ShutdownServices_withContextCancellation(b *testing.B) {
	benchmarkFn := func(b *testing.B, services []Service, timeout time.Duration) {
		b.Helper()

		for b.Loop() {
			app := New(Config{
				Services: services,
			})

			err := app.startServices(context.Background())
			if err != nil {
				b.Fatal("Expected no error during startup but got", err)
			}

			ctx, cancel := context.WithTimeout(context.Background(), timeout)
			err = app.shutdownServices(ctx)
			// We expect an error here due to the short timeout
			if err == nil && timeout < time.Second {
				b.Fatal("Expected error due to context cancellation but got none")
			}
			cancel()
		}
	}

	b.Run("single-service/immediate-cancellation", func(b *testing.B) {
		benchmarkFn(b, []Service{
			&mockService{name: "dep1", terminateDelay: 100 * time.Millisecond},
		}, 10*time.Millisecond)
	})

	b.Run("multiple-services/immediate-cancellation", func(b *testing.B) {
		benchmarkFn(b, []Service{
			&mockService{name: "dep1", terminateDelay: 100 * time.Millisecond},
			&mockService{name: "dep2", terminateDelay: 200 * time.Millisecond},
			&mockService{name: "dep3", terminateDelay: 300 * time.Millisecond},
		}, 10*time.Millisecond)
	})

	b.Run("multiple-services/successful-completion", func(b *testing.B) {
		const timeout = 500 * time.Millisecond

		for b.Loop() {
			app := New(Config{
				Services: []Service{
					&mockService{name: "dep1", terminateDelay: 10 * time.Millisecond},
					&mockService{name: "dep2", terminateDelay: 20 * time.Millisecond},
					&mockService{name: "dep3", terminateDelay: 30 * time.Millisecond},
				},
			})

			err := app.startServices(context.Background())
			if err != nil {
				b.Fatal("Expected no error but got", err)
			}

			ctx, cancel := context.WithTimeout(context.Background(), timeout)
			err = app.shutdownServices(ctx)
			if err != nil {
				b.Fatal("Expected no error but got", err)
			}
			cancel()
		}
	})
}

func Benchmark_ServicesMemory(b *testing.B) {
	benchmarkFn := func(b *testing.B, services []Service) {
		b.Helper()

		b.ReportAllocs()

		var err error
		for b.Loop() {
			app := New(Config{
				Services: services,
			})

			ctx := context.Background()
			err = app.startServices(ctx)
			if err != nil {
				continue
			}
			err = app.shutdownServices(ctx)
		}
		require.NoError(b, err)
	}

	b.Run("no-services", func(b *testing.B) {
		benchmarkFn(b, []Service{})
	})

	b.Run("single-service", func(b *testing.B) {
		benchmarkFn(b, []Service{
			&mockService{name: "dep1"},
		})
	})

	b.Run("multiple-services", func(b *testing.B) {
		benchmarkFn(b, []Service{
			&mockService{name: "dep1"},
			&mockService{name: "dep2"},
			&mockService{name: "dep3"},
		})
	})
}
