package scheduler

import (
	"context"
	"sync"
	"testing"
	"testing/synctest"
	"time"

	"hyapp/services/cron-service/internal/config"
	mysqlstorage "hyapp/services/cron-service/internal/storage/mysql"
)

type synctestStore struct {
	mu       sync.Mutex
	leases   int
	started  []mysqlstorage.TaskRun
	finished []mysqlstorage.TaskRun
}

type staticAppCodeProvider []string

func (p staticAppCodeProvider) ListEnabledAppCodes(context.Context) ([]string, error) {
	return append([]string(nil), p...), nil
}

func (s *synctestStore) TryAcquireTaskLease(_ context.Context, _ string, _ string, _ string, _ int64, _ time.Duration) (bool, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.leases++
	return true, nil
}

func (s *synctestStore) StartTaskRun(_ context.Context, run mysqlstorage.TaskRun) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.started = append(s.started, run)
	return nil
}

func (s *synctestStore) FinishTaskRun(_ context.Context, run mysqlstorage.TaskRun) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.finished = append(s.finished, run)
	return nil
}

// TestSchedulerRunUsesSynctestClock 锁定 cron loop 的首轮立即执行和 interval 退避。
// synctest 的虚拟时钟让 worker/timer 测试不依赖真实 sleep，避免调度抖动污染 CI。
func TestSchedulerRunUsesSynctestClock(t *testing.T) {
	synctest.Test(t, func(t *testing.T) {
		store := &synctestStore{}
		ctx, cancel := context.WithCancel(t.Context())
		defer cancel()

		var mu sync.Mutex
		runTimes := make([]time.Time, 0, 2)
		s := New("node-a", map[string]config.TaskConfig{
			"room_outbox": {
				Enabled:   true,
				Interval:  "1s",
				Timeout:   "100ms",
				LockTTL:   "5s",
				BatchSize: 8,
			},
		}, store, map[string]Handler{
			"room_outbox": func(_ context.Context, request BatchRequest) (BatchResult, error) {
				mu.Lock()
				defer mu.Unlock()
				if request.BatchSize != 8 || request.AppCode != "lalu" || request.WorkerID != "node-a" {
					t.Fatalf("unexpected request: %+v", request)
				}
				runTimes = append(runTimes, time.Now())
				return BatchResult{ProcessedCount: 1}, nil
			},
		}, staticAppCodeProvider{"lalu"})

		go s.Run(ctx)
		synctest.Wait()
		mu.Lock()
		if len(runTimes) != 1 {
			t.Fatalf("first run count = %d, want 1", len(runTimes))
		}
		firstRunAt := runTimes[0]
		mu.Unlock()

		time.Sleep(time.Second)
		synctest.Wait()
		mu.Lock()
		if len(runTimes) != 2 {
			t.Fatalf("second run count = %d, want 2", len(runTimes))
		}
		if got := runTimes[1].Sub(firstRunAt); got != time.Second {
			t.Fatalf("interval = %s, want 1s", got)
		}
		mu.Unlock()

		cancel()
		synctest.Wait()
		store.mu.Lock()
		defer store.mu.Unlock()
		if store.leases != 2 || len(store.started) != 2 || len(store.finished) != 2 {
			t.Fatalf("store calls: leases=%d started=%d finished=%d", store.leases, len(store.started), len(store.finished))
		}
	})
}

func TestSchedulerDiscoversEveryEnabledTenant(t *testing.T) {
	s := New("node-a", nil, nil, nil, staticAppCodeProvider{" huwaa ", "lalu", "huwaa", "fami", ""})

	appCodes, err := s.enabledAppCodes(context.Background())
	if err != nil {
		t.Fatalf("enabledAppCodes failed: %v", err)
	}
	want := []string{"fami", "huwaa", "lalu"}
	if len(appCodes) != len(want) {
		t.Fatalf("app code count mismatch: got=%v want=%v", appCodes, want)
	}
	for index := range want {
		if appCodes[index] != want[index] {
			t.Fatalf("app code mismatch: got=%v want=%v", appCodes, want)
		}
	}
}
