package schedule

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"go.uber.org/zap"
	"go.uber.org/zap/zaptest"
)

// MockJob is a mock implementation of the Job interface for testing.
type MockJob struct {
	RunFunc func(ctx context.Context) error
}

func (m *MockJob) Run(ctx context.Context) error {
	return m.RunFunc(ctx)
}

// MockHandler is a mock handler function for testing.
func MockHandler(ctx context.Context) error {
	return nil
}

func TestNewScheduler(t *testing.T) {
	s := NewScheduler()
	assert.NotNil(t, s)
	assert.Equal(t, len(s.workers), 0)
}

func TestAddJob(t *testing.T) {
	logger := zaptest.NewLogger(t)
	defer logger.Sync()

	s := NewScheduler()
	job := &MockJob{
		RunFunc: func(ctx context.Context) error {
			return nil
		},
	}
	s.AddJob(logger, time.Second, job)
	assert.Equal(t, len(s.workers), 1)
	worker, ok := s.workers[0].(*PeriodWorker)
	assert.True(t, ok)
	assert.NotNil(t, worker.runnable)
}

func TestSchedulerRun(t *testing.T) {
	logger := zaptest.NewLogger(t)
	defer logger.Sync()

	s := NewScheduler()

	runCalled := make(chan bool, 1)
	job := &MockJob{
		RunFunc: func(ctx context.Context) error {
			if len(runCalled) == 0 {
				runCalled <- true
			}
			return nil
		},
	}
	s.AddJob(logger, 100*time.Millisecond, job)

	go s.Run()
	time.Sleep(100 * time.Millisecond)
	assert.True(t, <-runCalled)

	// Cleanup
	for _, worker := range s.workers {
		worker.Stop()
	}
}

func TestPeriodWorker_Stop(t *testing.T) {
	logger := zaptest.NewLogger(t)
	defer logger.Sync()

	runCalled := make(chan bool, 1)
	job := &MockJob{
		RunFunc: func(ctx context.Context) error {
			if len(runCalled) == 0 {
				runCalled <- true
			}
			return nil
		},
	}
	worker := NewPeriodWorker(logger, 200*time.Millisecond, job.Run)
	// set firstTime to false , so it will not  run immediately
	worker.fistTime = false
	worker.Start()

	// Stop the worker before it runs
	worker.Stop()
	assert.Empty(t, runCalled)
}

// func TestPeriodWorker_RunWithError(t *testing.T) {
// 	logger := zaptest.NewLogger(t)
// 	defer logger.Sync()

// 	expectedErr := errors.New("job failed")
// 	runCalled := make(chan error)
// 	job := &MockJob{
// 		RunFunc: func(ctx context.Context) error {
// 			runCalled <- expectedErr
// 			return expectedErr
// 		},
// 	}
// 	worker := NewPeriodWorker(logger, 100*time.Millisecond, job.Run)
// 	worker.Start()

// 	select {
// 	case err := <-runCalled:
// 		if err != expectedErr {
// 			t.Fatalf("Expected error %v, got %v", expectedErr, err)
// 		}
// 	case <-time.After(500 * time.Millisecond):
// 		t.Fatal("Expected job Run to be called with error, but it wasn't")
// 	}

// 	// Cleanup
// 	worker.Stop()
// }

func TestNewPeriodWorkerWithDelay(t *testing.T) {
	logger := zaptest.NewLogger(t)
	defer logger.Sync()

	delay := 200 * time.Millisecond
	interval := 100 * time.Millisecond
	worker := NewPeriodWorkerWithDelay(logger, delay, interval, MockHandler)
	assert.Equal(t, worker.initialDelay, delay)
	assert.Equal(t, worker.interval, interval)
	assert.NotNil(t, worker.runnable)
}

func TestPeriodWorker_FirstRunDelay(t *testing.T) {
	logger := zaptest.NewLogger(t)
	defer logger.Sync()

	startTime := time.Now()
	runCalled := make(chan time.Duration, 1)
	job := &MockJob{
		RunFunc: func(ctx context.Context) error {
			if len(runCalled) == 0 {
				runCalled <- time.Since(startTime)
			}
			return nil
		},
	}

	worker := NewPeriodWorkerWithDelay(logger, 100*time.Millisecond, 500*time.Millisecond, job.Run)
	worker.Start()
	time.Sleep(500 * time.Millisecond)
	worker.Stop()
	select {
	case elapsed := <-runCalled:
		logger.Info("elapsed", zap.Duration("elapsed", elapsed))
		assert.True(t, elapsed <= 500*time.Millisecond)
	case <-time.After(100 * time.Millisecond):
		assert.FailNow(t, "Expected job Run to be called after delay, but it wasn't")
	}
}
