package app

import (
	"context"
	"errors"
	"sync"
	"testing"
	"time"

	"galaxy/gateway/internal/config"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

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

	application := New(config.Config{ShutdownTimeout: 50 * time.Millisecond})
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	resultCh := make(chan error, 1)
	go func() {
		resultCh <- application.Run(ctx)
	}()

	select {
	case err := <-resultCh:
		require.FailNowf(t, "Run() returned early", "error=%v", err)
	case <-time.After(50 * time.Millisecond):
	}

	cancel()

	select {
	case err := <-resultCh:
		require.NoError(t, err)
	case <-time.After(time.Second):
		require.FailNow(t, "Run() did not return after cancellation")
	}
}

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

	first := newLifecycleComponent()
	second := newLifecycleComponent()

	application := New(
		config.Config{ShutdownTimeout: time.Second},
		first,
		second,
	)

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	resultCh := make(chan error, 1)
	go func() {
		resultCh <- application.Run(ctx)
	}()

	first.waitStarted(t)
	second.waitStarted(t)

	cancel()

	select {
	case err := <-resultCh:
		require.NoError(t, err)
	case <-time.After(time.Second):
		require.FailNow(t, "Run() did not return after cancellation")
	}

	first.waitRunExited(t)
	second.waitRunExited(t)

	assert.Equal(t, 1, first.shutdownCalls())
	assert.Equal(t, 1, second.shutdownCalls())
}

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

	runErr := errors.New("boom")
	failing := newFailingComponent(runErr)
	blocking := newLifecycleComponent()

	application := New(
		config.Config{ShutdownTimeout: time.Second},
		failing,
		blocking,
	)

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	resultCh := make(chan error, 1)
	go func() {
		resultCh <- application.Run(ctx)
	}()

	failing.waitStarted(t)
	blocking.waitStarted(t)
	failing.releaseRun()

	select {
	case err := <-resultCh:
		require.Error(t, err)
		assert.ErrorIs(t, err, runErr)
	case <-time.After(time.Second):
		require.FailNow(t, "Run() did not return after component failure")
	}

	failing.waitRunExited(t)
	blocking.waitRunExited(t)

	assert.Equal(t, 1, failing.shutdownCalls())
	assert.Equal(t, 1, blocking.shutdownCalls())
}

// lifecycleComponent blocks in Run until the application calls Shutdown.
type lifecycleComponent struct {
	startedCh   chan struct{}
	runDoneCh   chan struct{}
	stopCh      chan struct{}
	shutdownMu  sync.Mutex
	shutdownCnt int
}

// newLifecycleComponent builds a component that exits Run only after Shutdown
// signals its stop channel.
func newLifecycleComponent() *lifecycleComponent {
	return &lifecycleComponent{
		startedCh: make(chan struct{}),
		runDoneCh: make(chan struct{}),
		stopCh:    make(chan struct{}),
	}
}

// Run marks the component as started, waits for cancellation, and then blocks
// until Shutdown releases the stop channel.
func (c *lifecycleComponent) Run(ctx context.Context) error {
	close(c.startedCh)
	defer close(c.runDoneCh)

	<-ctx.Done()
	<-c.stopCh
	return nil
}

// Shutdown records the call and releases the run loop.
func (c *lifecycleComponent) Shutdown(context.Context) error {
	c.shutdownMu.Lock()
	defer c.shutdownMu.Unlock()

	c.shutdownCnt++
	if c.shutdownCnt == 1 {
		close(c.stopCh)
	}

	return nil
}

// waitStarted blocks until Run has started or fails the test on timeout.
func (c *lifecycleComponent) waitStarted(t *testing.T) {
	t.Helper()

	select {
	case <-c.startedCh:
	case <-time.After(time.Second):
		require.FailNow(t, "component did not start")
	}
}

// waitRunExited blocks until Run exits or fails the test on timeout.
func (c *lifecycleComponent) waitRunExited(t *testing.T) {
	t.Helper()

	select {
	case <-c.runDoneCh:
	case <-time.After(time.Second):
		require.FailNow(t, "component run did not exit")
	}
}

// shutdownCalls returns the number of observed Shutdown invocations.
func (c *lifecycleComponent) shutdownCalls() int {
	c.shutdownMu.Lock()
	defer c.shutdownMu.Unlock()

	return c.shutdownCnt
}

// failingComponent returns a predefined error once released by the test and
// still tracks shutdown calls.
type failingComponent struct {
	startedCh   chan struct{}
	releaseCh   chan struct{}
	runDoneCh   chan struct{}
	shutdownMu  sync.Mutex
	shutdownCnt int
	err         error
}

// newFailingComponent builds a component whose Run returns err after release.
func newFailingComponent(err error) *failingComponent {
	return &failingComponent{
		startedCh: make(chan struct{}),
		releaseCh: make(chan struct{}),
		runDoneCh: make(chan struct{}),
		err:       err,
	}
}

// Run waits until the test releases it and then returns the configured error.
func (c *failingComponent) Run(context.Context) error {
	close(c.startedCh)
	defer close(c.runDoneCh)

	<-c.releaseCh
	return c.err
}

// Shutdown records that the application attempted graceful shutdown.
func (c *failingComponent) Shutdown(context.Context) error {
	c.shutdownMu.Lock()
	defer c.shutdownMu.Unlock()

	c.shutdownCnt++
	return nil
}

// waitStarted blocks until Run has started or fails the test on timeout.
func (c *failingComponent) waitStarted(t *testing.T) {
	t.Helper()

	select {
	case <-c.startedCh:
	case <-time.After(time.Second):
		require.FailNow(t, "failing component did not start")
	}
}

// releaseRun allows Run to return its configured error.
func (c *failingComponent) releaseRun() {
	close(c.releaseCh)
}

// waitRunExited blocks until Run exits or fails the test on timeout.
func (c *failingComponent) waitRunExited(t *testing.T) {
	t.Helper()

	select {
	case <-c.runDoneCh:
	case <-time.After(time.Second):
		require.FailNow(t, "failing component run did not exit")
	}
}

// shutdownCalls returns the number of observed Shutdown invocations.
func (c *failingComponent) shutdownCalls() int {
	c.shutdownMu.Lock()
	defer c.shutdownMu.Unlock()

	return c.shutdownCnt
}
