package controller

import (
	"context"
	"errors"
	"sync/atomic"
	"testing"
	"time"
)

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

	ctx, cancel := context.WithCancel(context.Background())
	t.Cleanup(cancel)

	runCh := make(chan struct{})
	stopCh := make(chan struct{})
	errCh := make(chan error)

	task := func(taskCtx context.Context) error {
		runCh <- struct{}{}

		select {
		case err := <-errCh:
			return err
		case <-taskCtx.Done():
			stopCh <- struct{}{}
			return taskCtx.Err()
		}
	}

	lease := newTestLease()

	go newSupervisor(task, lease).run(ctx)

	select {
	case <-runCh:
		t.Fatal("task should not be running before lease is held")
	case <-time.After(500 * time.Millisecond):
	}

	lease.acquired()

	select {
	case <-runCh:
	case <-time.After(500 * time.Millisecond):
		t.Fatal("task not running after lease is acquired")
	}

	select {
	case <-stopCh:
		t.Fatal("task should not have stopped before lease is lost")
	case <-time.After(500 * time.Millisecond):
	}

	lease.lost()

	select {
	case <-stopCh:
	case <-time.After(500 * time.Millisecond):
		t.Fatal("task still running after lease was lost")
	}

	select {
	case <-runCh:
		t.Fatal("task should not be run again before lease is re-acquired")
	case <-time.After(500 * time.Millisecond):
	}

	lease.acquired()

	select {
	case <-runCh:
	case <-time.After(500 * time.Millisecond):
		t.Fatal("task not running after lease is re-acquired")
	}

	errCh <- errors.New("KABOOM")

	select {
	case <-runCh:
	case <-time.After(2 * time.Second):
		t.Fatal("task was not restarted")
	}

	cancel()

	select {
	case <-stopCh:
	case <-time.After(500 * time.Millisecond):
		t.Fatal("task still running after parent context was canceled")
	}
}

func newTestLease() *testLease {
	return &testLease{ch: make(chan struct{}, 1)}
}

type testLease struct {
	held atomic.Bool
	ch   chan struct{}
}

func (l *testLease) Held() bool               { return l.held.Load() }
func (l *testLease) Changed() <-chan struct{} { return l.ch }

func (l *testLease) acquired() { l.setHeld(true) }
func (l *testLease) lost()     { l.setHeld(false) }

func (l *testLease) setHeld(held bool) {
	l.held.Store(held)

	select {
	case l.ch <- struct{}{}:
	default:
	}
}
