package healthfile_test

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

	"gitea.dwysokinski.me/twhelp/corev3/internal/health"
	"gitea.dwysokinski.me/twhelp/corev3/internal/health/healthfile"
	"gitea.dwysokinski.me/twhelp/corev3/internal/health/healthtest"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

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

	testObserver(t, func(t *testing.T) *healthfile.Observer {
		t.Helper()

		h := health.New(
			health.WithLiveCheck(healthtest.Checker{Nam: "test"}),
			health.WithLiveCheck(healthtest.Checker{Nam: "test2"}),
		)

		dir := t.TempDir()
		livePath := path.Join(dir, "live")

		o := healthfile.LiveObserver(h, livePath)
		t.Cleanup(func() {
			_ = o.Close()
		})

		return o
	})
}

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

	testObserver(t, func(t *testing.T) *healthfile.Observer {
		t.Helper()

		h := health.New(
			health.WithReadyCheck(healthtest.Checker{Nam: "test"}),
			health.WithReadyCheck(healthtest.Checker{Nam: "test2"}),
		)

		dir := t.TempDir()
		readyPath := path.Join(dir, "ready")

		o := healthfile.ReadyObserver(h, readyPath)
		t.Cleanup(func() {
			_ = o.Close()
		})

		return o
	})
}

func testObserver(t *testing.T, newObserver func(t *testing.T) *healthfile.Observer) {
	t.Helper()

	t.Run("standard run/close path", func(t *testing.T) {
		t.Parallel()

		o := newObserver(t)

		observerStopped := make(chan struct{})
		go func() {
			defer close(observerStopped)
			ctx, cancel := context.WithTimeout(context.Background(), time.Second)
			defer cancel()
			_ = o.Run(ctx)
		}()

		assert.EventuallyWithT(t, func(collect *assert.CollectT) {
			assert.FileExists(collect, o.Path())
		}, time.Second, 10*time.Millisecond)

		require.NoError(t, o.Close())

		select {
		case <-observerStopped:
		// OK
		default:
			assert.Fail(t, "Run goroutine not stopped")
		}
	})

	t.Run("observer respects ctx.Done", func(t *testing.T) {
		t.Parallel()

		o := newObserver(t)

		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
		defer cancel()
		require.ErrorIs(t, o.Run(ctx), context.DeadlineExceeded)
	})

	t.Run("run can only be called once", func(t *testing.T) {
		t.Parallel()

		expectedErrs := 2

		o := newObserver(t)

		ctx, cancel := context.WithTimeout(context.Background(), time.Second)
		defer cancel()

		var wg sync.WaitGroup
		errCh := make(chan error)

		for i := 1; i <= 1+expectedErrs; i++ {
			wg.Add(1)
			go func() {
				defer wg.Done()
				errCh <- o.Run(ctx)
			}()
		}

		go func() {
			wg.Wait()
			close(errCh)
		}()

		var cnt int

		for err := range errCh {
			if errors.Is(err, healthfile.ErrAlreadyRunning) {
				cnt++
			}

			if cnt == expectedErrs {
				require.NoError(t, o.Close())
			}
		}

		assert.Equal(t, expectedErrs, cnt)
	})
}
