package queue_test

import (
	"context"
	"errors"
	"sync"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"

	"security-center-backend/internal/config"
	"security-center-backend/internal/modules/caasm/mocks"
	"security-center-backend/internal/modules/caasm/queue"
	"security-center-backend/internal/pkg/logger"
)

type workerLifecycleQueueStub struct {
	mu           sync.Mutex
	requeuedJobs int
	ackedJobs    int
}

func (s *workerLifecycleQueueStub) Enqueue(context.Context, *queue.SyncJobPayload) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.requeuedJobs++
	return nil
}

func (s *workerLifecycleQueueStub) Acknowledge(context.Context, string) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.ackedJobs++
	return nil
}

func (s *workerLifecycleQueueStub) Fail(context.Context, *queue.SyncJobPayload, string, error) error {
	return nil
}

func (s *workerLifecycleQueueStub) GetQueueDepth(context.Context) (int64, error) { return 0, nil }
func (s *workerLifecycleQueueStub) GetDLQDepth(context.Context) (int64, error)   { return 0, nil }
func (s *workerLifecycleQueueStub) CleanupStuckJobs(context.Context) error       { return nil }
func (s *workerLifecycleQueueStub) ExistsActiveAccount(context.Context, uint64) bool {
	return false
}
func (s *workerLifecycleQueueStub) Dequeue(context.Context, string) (*queue.SyncJobPayload, string, error) {
	time.Sleep(2 * time.Millisecond)
	return nil, "", nil
}

func TestWorkerPool_ProcessJob(t *testing.T) {
	// Setup
	mockQueue := new(mocks.MockJobQueue)
	log := logger.New(config.LogConfig{Level: "error", Format: "text"})

	executorCalled := false
	executor := func(ctx context.Context, job *queue.SyncJobPayload) error {
		executorCalled = true
		return nil
	}

	wp := queue.NewWorkerPool(mockQueue, 1, log, executor)

	job := &queue.SyncJobPayload{
		JobID:          "job-123",
		CloudAccountID: 1,
		JobType:        queue.JobTypeCloudAccountSync,
	}

	mockQueue.On("Acknowledge", mock.Anything, "stream-123").Return(nil)

	// Execute via exported test method
	wp.ProcessJob(job, "stream-123", "worker-1")

	// Assert
	assert.True(t, executorCalled)
	mockQueue.AssertExpectations(t)
}

func TestWorkerPool_ProcessJob_Failure(t *testing.T) {
	mockQueue := new(mocks.MockJobQueue)
	log := logger.New(config.LogConfig{Level: "error", Format: "text"})

	executor := func(ctx context.Context, job *queue.SyncJobPayload) error {
		return errors.New("sync failed")
	}

	wp := queue.NewWorkerPool(mockQueue, 1, log, executor)

	job := &queue.SyncJobPayload{
		JobID:          "job-123",
		CloudAccountID: 1,
	}

	mockQueue.On("Fail", mock.Anything, job, "stream-123", mock.MatchedBy(func(err error) bool {
		return err.Error() == "sync failed"
	})).Return(nil)

	wp.ProcessJob(job, "stream-123", "worker-1")

	mockQueue.AssertExpectations(t)
}

func TestWorkerPool_ProcessJob_DefersWhenAdmissionRejects(t *testing.T) {
	mockQueue := new(mocks.MockJobQueue)
	log := logger.New(config.LogConfig{Level: "error", Format: "text"})

	executorCalled := false
	executor := func(ctx context.Context, job *queue.SyncJobPayload) error {
		executorCalled = true
		return nil
	}

	wp := queue.NewWorkerPool(mockQueue, 1, log, executor)
	wp.SetAdmissionController(func(ctx context.Context, job *queue.SyncJobPayload, activeLocalJobs int) queue.AdmissionDecision {
		return queue.AdmissionDecision{
			Allowed: false,
			Delay:   0,
			Reason:  "runtime_capacity_guard",
		}
	})

	job := &queue.SyncJobPayload{
		JobID:          "job-123",
		CloudAccountID: 1,
		SyncJobID:      ptrUint64(99),
	}

	mockQueue.On("Enqueue", mock.Anything, job).Return(nil)
	mockQueue.On("Acknowledge", mock.Anything, "stream-123").Return(nil)

	start := time.Now()
	wp.ProcessJob(job, "stream-123", "worker-1")

	assert.False(t, executorCalled)
	assert.Less(t, time.Since(start), 20*time.Second)
	mockQueue.AssertExpectations(t)
}

func TestWorkerPool_ProcessJob_DefersWhenDBGuardRejects(t *testing.T) {
	mockQueue := new(mocks.MockJobQueue)
	log := logger.New(config.LogConfig{Level: "error", Format: "text"})

	executorCalled := false
	executor := func(ctx context.Context, job *queue.SyncJobPayload) error {
		executorCalled = true
		return nil
	}

	wp := queue.NewWorkerPool(mockQueue, 1, log, executor)
	wp.SetAdmissionController(func(ctx context.Context, job *queue.SyncJobPayload, activeLocalJobs int) queue.AdmissionDecision {
		return queue.AdmissionDecision{
			Allowed: false,
			Delay:   0,
			Reason:  "db_active_account_guard",
		}
	})

	job := &queue.SyncJobPayload{
		JobID:          "job-123",
		CloudAccountID: 1,
	}

	mockQueue.On("Enqueue", mock.Anything, job).Return(nil)
	mockQueue.On("Acknowledge", mock.Anything, "stream-123").Return(nil)

	start := time.Now()
	wp.ProcessJob(job, "stream-123", "worker-1")

	assert.False(t, executorCalled)
	assert.Less(t, time.Since(start), 35*time.Second)
	mockQueue.AssertExpectations(t)
}

func TestWorkerPool_DeferDoesNotAckWhenRequeueFails(t *testing.T) {
	mockQueue := new(mocks.MockJobQueue)
	log := logger.New(config.LogConfig{Level: "error", Format: "text"})

	wp := queue.NewWorkerPool(mockQueue, 1, log, func(ctx context.Context, job *queue.SyncJobPayload) error {
		t.Fatal("executor should not run for rejected admission")
		return nil
	})
	wp.SetAdmissionController(func(ctx context.Context, job *queue.SyncJobPayload, activeLocalJobs int) queue.AdmissionDecision {
		return queue.AdmissionDecision{
			Allowed: false,
			Delay:   0,
			Reason:  "runtime_capacity_guard",
		}
	})

	job := &queue.SyncJobPayload{
		JobID:          "job-requeue-fails",
		CloudAccountID: 1,
	}

	mockQueue.On("Enqueue", mock.Anything, job).Return(errors.New("redis unavailable"))

	wp.ProcessJob(job, "stream-123", "worker-1")

	mockQueue.AssertExpectations(t)
	mockQueue.AssertNotCalled(t, "Acknowledge", mock.Anything, "stream-123")
}

func TestWorkerPool_ProcessJob_ReportsDeferredJobEvent(t *testing.T) {
	mockQueue := new(mocks.MockJobQueue)
	log := logger.New(config.LogConfig{Level: "error", Format: "text"})

	wp := queue.NewWorkerPool(mockQueue, 1, log, func(ctx context.Context, job *queue.SyncJobPayload) error {
		t.Fatal("executor should not run for deferred job")
		return nil
	})
	wp.SetAdmissionController(func(ctx context.Context, job *queue.SyncJobPayload, activeLocalJobs int) queue.AdmissionDecision {
		return queue.AdmissionDecision{
			Allowed: false,
			Delay:   0,
			Reason:  "runtime_capacity_guard",
			Metadata: map[string]interface{}{
				"coordination_state": "runtime_capacity_guard",
			},
		}
	})

	reported := false
	wp.SetEventReporter(func(ctx context.Context, syncJobID uint64, eventType string, message string, metadata map[string]interface{}) {
		reported = true
		assert.Equal(t, uint64(99), syncJobID)
		assert.Equal(t, "warning", eventType)
		assert.Equal(t, "Job deferred by runtime coordination guard", message)
		assert.Equal(t, "runtime_capacity_guard", metadata["reason"])
		assert.Equal(t, "coordination_guard", metadata["slowdown_source"])
		assert.Equal(t, "runtime_capacity_guard", metadata["coordination_state"])
	})

	job := &queue.SyncJobPayload{
		JobID:          "job-123",
		CloudAccountID: 1,
		SyncJobID:      ptrUint64(99),
	}

	mockQueue.On("Enqueue", mock.Anything, job).Return(nil)
	mockQueue.On("Acknowledge", mock.Anything, "stream-123").Return(nil)

	wp.ProcessJob(job, "stream-123", "worker-1")

	assert.True(t, reported)
	mockQueue.AssertExpectations(t)
}

func TestWorkerPoolCancelSyncJobCancelsActiveExecutor(t *testing.T) {
	mockQueue := new(mocks.MockJobQueue)
	log := logger.New(config.LogConfig{Level: "error", Format: "text"})

	executorStarted := make(chan struct{})
	executorReleased := make(chan struct{})
	failCalled := make(chan struct{})
	syncJobID := uint64(321)

	wp := queue.NewWorkerPool(mockQueue, 1, log, func(ctx context.Context, job *queue.SyncJobPayload) error {
		close(executorStarted)
		<-ctx.Done()
		close(executorReleased)
		return ctx.Err()
	})

	job := &queue.SyncJobPayload{
		JobID:          "job-321",
		CloudAccountID: 1,
		SyncJobID:      &syncJobID,
	}

	mockQueue.On("Fail", mock.Anything, job, "stream-321", mock.Anything).Run(func(args mock.Arguments) {
		close(failCalled)
	}).Return(nil)

	go wp.ProcessJob(job, "stream-321", "worker-1")
	<-executorStarted

	assert.True(t, wp.CancelSyncJob(syncJobID))

	select {
	case <-executorReleased:
	case <-time.After(2 * time.Second):
		t.Fatal("expected executor context to be cancelled")
	}
	select {
	case <-failCalled:
	case <-time.After(2 * time.Second):
		t.Fatal("expected cancelled job to be reported as failed")
	}

	mockQueue.AssertExpectations(t)
}

func TestWorkerPoolDeferJobDoesNotBlockStopAndWorkerCanRestart(t *testing.T) {
	queueStub := &workerLifecycleQueueStub{}
	log := logger.New(config.LogConfig{Level: "error", Format: "text"})

	wp := queue.NewWorkerPool(queueStub, 1, log, func(ctx context.Context, job *queue.SyncJobPayload) error {
		return nil
	})
	wp.SetAdmissionController(func(ctx context.Context, job *queue.SyncJobPayload, activeLocalJobs int) queue.AdmissionDecision {
		return queue.AdmissionDecision{
			Allowed: false,
			Delay:   time.Minute,
			Reason:  "runtime_capacity_guard",
		}
	})
	wp.Start()

	job := &queue.SyncJobPayload{
		JobID:          "job-deferred",
		CloudAccountID: 1,
	}

	done := make(chan struct{})
	go func() {
		defer close(done)
		wp.ProcessJob(job, "stream-deferred", "worker-1")
	}()

	time.Sleep(10 * time.Millisecond)
	stopStart := time.Now()
	wp.Stop()
	assert.Less(t, time.Since(stopStart), 2*time.Second)

	select {
	case <-done:
	case <-time.After(2 * time.Second):
		t.Fatal("expected deferred job processing to exit during stop")
	}

	wp.Start()
	wp.Stop()

	queueStub.mu.Lock()
	defer queueStub.mu.Unlock()
	assert.GreaterOrEqual(t, queueStub.requeuedJobs, 1)
	assert.GreaterOrEqual(t, queueStub.ackedJobs, 1)
}

func ptrUint64(v uint64) *uint64 {
	return &v
}
