package crontab

import (
	"context"
	"io"
	"log"
	"sync"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"senlinai-agent/backend/internal/models"
)

func TestDatasetSchedulerCollectsImmediatelyAndRepeats(t *testing.T) {
	collector := &recordingDatasetCollector{calls: make(chan time.Time, 3)}
	scheduler := &DatasetScheduler{
		collector: collector,
		interval:  10 * time.Millisecond,
		logger:    log.New(io.Discard, "", 0),
	}
	ctx, cancel := context.WithCancel(context.Background())
	done := make(chan struct{})
	go func() {
		defer close(done)
		scheduler.Run(ctx)
	}()

	first := receiveCollectionCall(t, collector.calls)
	second := receiveCollectionCall(t, collector.calls)
	require.GreaterOrEqual(t, second.Sub(first), 8*time.Millisecond)

	cancel()
	require.Eventually(t, func() bool {
		select {
		case <-done:
			return true
		default:
			return false
		}
	}, time.Second, 5*time.Millisecond)
}

func TestDatasetSchedulerStopsWhenContextIsAlreadyCanceled(t *testing.T) {
	collector := &recordingDatasetCollector{calls: make(chan time.Time, 1)}
	scheduler := &DatasetScheduler{
		collector: collector,
		interval:  10 * time.Millisecond,
		logger:    log.New(io.Discard, "", 0),
	}
	ctx, cancel := context.WithCancel(context.Background())
	cancel()

	scheduler.Run(ctx)

	require.Zero(t, collector.callCount())
}

func TestDatasetSchedulerSkipsTicksWhileCollectionIsRunning(t *testing.T) {
	collector := &blockingDatasetCollector{
		started: make(chan struct{}, 2),
		release: make(chan struct{}),
	}
	scheduler := &DatasetScheduler{
		collector: collector,
		interval:  5 * time.Millisecond,
		logger:    log.New(io.Discard, "", 0),
	}
	ctx, cancel := context.WithCancel(context.Background())
	done := make(chan struct{})
	go func() {
		defer close(done)
		scheduler.Run(ctx)
	}()

	select {
	case <-collector.started:
	case <-time.After(time.Second):
		t.Fatal("timed out waiting for first collection")
	}
	time.Sleep(20 * time.Millisecond)
	require.Equal(t, 1, collector.callCount())

	close(collector.release)
	require.Eventually(t, func() bool {
		return collector.callCount() >= 2
	}, time.Second, 5*time.Millisecond)
	cancel()
	<-done
}

type recordingDatasetCollector struct {
	mu    sync.Mutex
	count int
	calls chan time.Time
}

func (c *recordingDatasetCollector) SyncAllSources(context.Context) ([]models.SaDatasetCron, error) {
	now := time.Now()
	c.mu.Lock()
	c.count++
	c.mu.Unlock()
	c.calls <- now
	return []models.SaDatasetCron{{Status: "completed"}}, nil
}

func (c *recordingDatasetCollector) callCount() int {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.count
}

func receiveCollectionCall(t *testing.T, calls <-chan time.Time) time.Time {
	t.Helper()
	select {
	case calledAt := <-calls:
		return calledAt
	case <-time.After(time.Second):
		t.Fatal("timed out waiting for dataset collection")
		return time.Time{}
	}
}

type blockingDatasetCollector struct {
	mu      sync.Mutex
	count   int
	started chan struct{}
	release chan struct{}
}

func (c *blockingDatasetCollector) SyncAllSources(ctx context.Context) ([]models.SaDatasetCron, error) {
	c.mu.Lock()
	c.count++
	c.mu.Unlock()
	select {
	case c.started <- struct{}{}:
	default:
	}
	select {
	case <-c.release:
	case <-ctx.Done():
	}
	return nil, nil
}

func (c *blockingDatasetCollector) callCount() int {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.count
}
