package healthcheck

import (
	"fmt"
	"testing"
	"time"

	"github.com/onsi/gomega"
	"gorm.io/gorm"

	"github.com/pingcap-inc/clinic/pkg/db"
	"github.com/pingcap-inc/clinic/pkg/db/model"
	"github.com/pingcap-inc/clinic/pkg/task_manager/task"
	_import "github.com/pingcap-inc/clinic/pkg/task_manager/task/import"
)

func TestScannerScanPendingJobsBatchesToAvailableQueueSlots(t *testing.T) {
	g := gomega.NewWithT(t)
	repo := model.NewMockHealthCheckRepository()
	useHealthCheckRepository(t, repo)

	base := time.Now().Add(-time.Hour)
	for i := 0; i < 244; i++ {
		result := newPendingHealthCheckResult(i, base.Add(time.Duration(i)*time.Second))
		repo.Results[result.UUID] = result
	}

	scanner := NewScannerService(nil)
	scanner.scanPendingJobs()

	g.Expect(len(scanner.queue)).To(gomega.Equal(cap(scanner.queue)))
	g.Expect(scanner.inFlightCount()).To(gomega.Equal(cap(scanner.queue)))
	g.Expect(repo.CountPendingOrRunningHealthCheckJobsCalls).To(gomega.Equal(1))
	g.Expect(repo.FetchPendingOrRunningHealthCheckJobsBatchCalls).To(gomega.Equal(1))

	for i := 0; i < cap(scanner.queue); i++ {
		event := <-scanner.queue
		job, ok := event.(*Job)
		g.Expect(ok).To(gomega.BeTrue())
		g.Expect(job.result.UUID).To(gomega.Equal(fmt.Sprintf("job-%03d", i)))
	}
}

func TestScannerScanPendingJobsDoesNotFetchWhenQueueIsFull(t *testing.T) {
	g := gomega.NewWithT(t)
	repo := model.NewMockHealthCheckRepository()
	useHealthCheckRepository(t, repo)

	base := time.Now().Add(-time.Hour)
	for i := 0; i < 10; i++ {
		result := newPendingHealthCheckResult(i, base.Add(time.Duration(i)*time.Second))
		repo.Results[result.UUID] = result
	}

	scanner := NewScannerService(nil)
	for i := 0; i < cap(scanner.queue); i++ {
		scanner.queue <- noopEvent{}
	}

	scanner.scanPendingJobs()

	g.Expect(len(scanner.queue)).To(gomega.Equal(cap(scanner.queue)))
	g.Expect(scanner.inFlightCount()).To(gomega.Equal(0))
	g.Expect(repo.CountPendingOrRunningHealthCheckJobsCalls).To(gomega.Equal(1))
	g.Expect(repo.FetchPendingOrRunningHealthCheckJobsBatchCalls).To(gomega.Equal(0))
}

func TestScannerScanPendingJobsExcludesInFlightJobs(t *testing.T) {
	g := gomega.NewWithT(t)
	repo := model.NewMockHealthCheckRepository()
	useHealthCheckRepository(t, repo)

	base := time.Now().Add(-time.Hour)
	for i := 0; i < 3; i++ {
		result := newPendingHealthCheckResult(i, base.Add(time.Duration(i)*time.Second))
		repo.Results[result.UUID] = result
	}

	scanner := NewScannerService(nil)
	g.Expect(scanner.markInFlight("job-000")).To(gomega.BeTrue())

	scanner.scanPendingJobs()

	g.Expect(len(scanner.queue)).To(gomega.Equal(2))
	g.Expect(scanner.inFlightCount()).To(gomega.Equal(3))

	for _, expectedUUID := range []string{"job-001", "job-002"} {
		event := <-scanner.queue
		job, ok := event.(*Job)
		g.Expect(ok).To(gomega.BeTrue())
		g.Expect(job.result.UUID).To(gomega.Equal(expectedUUID))
	}
}

func TestScannerScanPendingJobsLimitsByInFlightCapacity(t *testing.T) {
	g := gomega.NewWithT(t)
	repo := model.NewMockHealthCheckRepository()
	useHealthCheckRepository(t, repo)

	base := time.Now().Add(-time.Hour)
	for i := 0; i < 105; i++ {
		result := newPendingHealthCheckResult(i, base.Add(time.Duration(i)*time.Second))
		repo.Results[result.UUID] = result
	}

	scanner := NewScannerService(nil)
	for i := 0; i < 10; i++ {
		g.Expect(scanner.markInFlight(fmt.Sprintf("job-%03d", i))).To(gomega.BeTrue())
	}

	scanner.scanPendingJobs()

	g.Expect(len(scanner.queue)).To(gomega.Equal(cap(scanner.queue) - 10))
	g.Expect(scanner.inFlightCount()).To(gomega.Equal(cap(scanner.queue)))
}

func TestScannerScanPendingJobsPrioritizesPendingBeforeRunningBacklog(t *testing.T) {
	g := gomega.NewWithT(t)
	repo := model.NewMockHealthCheckRepository()
	useHealthCheckRepository(t, repo)

	base := time.Now().Add(-time.Hour)
	for i := 0; i < _import.QueueSize; i++ {
		result := newPendingHealthCheckResult(i, base.Add(time.Duration(i)*time.Second))
		result.Status = string(db.StatusPartial)
		repo.Results[result.UUID] = result
	}
	newPending := newPendingHealthCheckResult(999, time.Now())
	repo.Results[newPending.UUID] = newPending

	scanner := NewScannerService(nil)
	scanner.scanPendingJobs()

	event := <-scanner.queue
	job, ok := event.(*Job)
	g.Expect(ok).To(gomega.BeTrue())
	g.Expect(job.result.UUID).To(gomega.Equal(newPending.UUID))
}

func TestScannerWorkerReleasesInFlightAfterJobFinishes(t *testing.T) {
	g := gomega.NewWithT(t)
	repo := model.NewMockHealthCheckRepository()
	useHealthCheckRepository(t, repo)

	result := newPendingHealthCheckResult(0, time.Now())
	result.Status = string(db.StatusSuccess)
	repo.Results[result.UUID] = result

	scanner := NewScannerService(nil)
	g.Expect(scanner.markInFlight(result.UUID)).To(gomega.BeTrue())
	t.Cleanup(func() {
		close(scanner.queue)
	})

	go scanner.startWorker(0)
	scanner.queue <- NewJob(nil, result)

	g.Eventually(scanner.inFlightCount, time.Second, 10*time.Millisecond).Should(gomega.Equal(0))
}

func TestScannerWorkerReleasesInFlightAfterJobPanic(t *testing.T) {
	g := gomega.NewWithT(t)
	repo := &panicGetHealthCheckRepository{MockHealthCheckRepository: model.NewMockHealthCheckRepository()}
	useHealthCheckRepository(t, repo)

	result := newPendingHealthCheckResult(0, time.Now())

	scanner := NewScannerService(nil)
	g.Expect(scanner.markInFlight(result.UUID)).To(gomega.BeTrue())
	t.Cleanup(func() {
		close(scanner.queue)
	})

	go scanner.startWorker(0)
	scanner.queue <- NewJob(nil, result)

	g.Eventually(scanner.inFlightCount, time.Second, 10*time.Millisecond).Should(gomega.Equal(0))
}

func useHealthCheckRepository(t *testing.T, repo model.HealthCheckRepository) {
	t.Helper()
	SetRepository(repo)
	t.Cleanup(func() {
		SetRepository(model.NewHealthCheckDB())
	})
}

func newPendingHealthCheckResult(index int, createdAt time.Time) *db.HealthCheckResult {
	return &db.HealthCheckResult{
		Model: gorm.Model{
			ID:        uint(index + 1),
			CreatedAt: createdAt,
		},
		UUID:      fmt.Sprintf("job-%03d", index),
		ClusterID: "cluster-1",
		Status:    string(db.StatusPending),
	}
}

type noopEvent struct{}

func (noopEvent) Run(task.K8sClient) error {
	return nil
}

type panicGetHealthCheckRepository struct {
	*model.MockHealthCheckRepository
}

func (r *panicGetHealthCheckRepository) GetHealthCheckResultByUUID(string) (*db.HealthCheckResult, error) {
	panic("panic for scanner in-flight release test")
}
