package agent

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

	"github.com/stretchr/testify/suite"
	rdutil "gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/remote_development/agent/util"
)

type ErrorTrackerTestSuite struct {
	suite.Suite

	tracker *errorDetailsTracker

	testError        error
	testErrorDetails *ErrorDetails
	testWorkspace    string
	testNamespace    string
	testTrackerKey   errorTrackerKey
}

func TestRemoteDevModuleErrorTracker(t *testing.T) {
	suite.Run(t, new(ErrorTrackerTestSuite))
}

func (e *ErrorTrackerTestSuite) TestErrorTracking_ErrorDetailsPublished() {
	ctx := context.Background()

	asyncErrorDetails, unblockPublish := e.newBlockedAsyncErrorDetails(e.testErrorDetails)
	e.NotContains(e.tracker.createSnapshot(), e.testTrackerKey)

	v1 := uint64(1)
	e.tracker.watchForLatestErrors(ctx, e.testWorkspace, e.testNamespace, v1, asyncErrorDetails)

	// since publishing of error is blocked, verify state of tracker
	e.ensureTrackerIsWatchingForErrors(e.testTrackerKey, v1)

	// resume error publish, wait for it to be received and verify state of tracker again
	unblockPublish()
	e.tracker.waitForErrors()

	e.ensureTrackerReceivedErrorDetails(e.testTrackerKey, v1, e.testErrorDetails)
}

func (e *ErrorTrackerTestSuite) TestErrorTracking_NilErrorDetails() {
	ctx := context.Background()

	// blocked value is created so that tracker state can be verified before publish
	asyncErrorDetails, unblockPublish := e.newBlockedAsyncErrorDetails(nil)

	e.NotContains(e.tracker.createSnapshot(), e.testTrackerKey)

	v1 := uint64(1)
	e.tracker.watchForLatestErrors(ctx, e.testWorkspace, e.testNamespace, v1, asyncErrorDetails)

	// since publishing of nil is blocked, verify state of tracker
	e.ensureTrackerIsWatchingForErrors(e.testTrackerKey, v1)

	// unblock the async nil and verify final state of tracker
	unblockPublish()
	e.tracker.waitForErrors()

	e.ensureTrackerIsNotWatchingKey(e.testTrackerKey)
}

func (e *ErrorTrackerTestSuite) TestErrorTracking_NoErrorDetails() {
	ctx := context.Background()

	asyncOp1 := rdutil.RunWithAsyncResult(func(_ chan<- *ErrorDetails) {
		// no errors are published to the write-only channel
	})

	e.NotContains(e.tracker.createSnapshot(), e.testTrackerKey)

	v1 := uint64(1)
	e.tracker.watchForLatestErrors(ctx, e.testWorkspace, e.testNamespace, v1, asyncOp1)

	// wait for errors(if any) and verify final state of tracker
	e.tracker.waitForErrors()

	e.ensureTrackerIsNotWatchingKey(e.testTrackerKey)
}

func (e *ErrorTrackerTestSuite) TestErrorTracking_MultipleErrorDetails() {
	ctx := context.Background()

	err1 := errors.New("applier error 1")
	errDetails1 := newErrorDetails(ErrorTypeApplier, err1.Error())

	err2 := errors.New("applier error 2")
	errDetails2 := newErrorDetails(ErrorTypeApplier, err2.Error())

	asyncOp1 := rdutil.RunWithAsyncResult(func(ch chan<- *ErrorDetails) {
		ch <- errDetails1
		ch <- errDetails2
	})

	v1 := uint64(1)
	e.tracker.watchForLatestErrors(ctx, e.testWorkspace, e.testNamespace, v1, asyncOp1)
	e.tracker.waitForErrors()

	// verify that the received error is the first error
	e.ensureTrackerReceivedErrorDetails(e.testTrackerKey, v1, errDetails1)
}

func (e *ErrorTrackerTestSuite) TestErrorTracking_MultipleNilErrors() {
	ctx := context.Background()

	asyncOp1 := rdutil.RunWithAsyncResult(func(ch chan<- *ErrorDetails) {
		ch <- nil
		ch <- nil
	})

	v1 := uint64(1)

	e.tracker.watchForLatestErrors(ctx, e.testWorkspace, e.testNamespace, v1, asyncOp1)
	e.tracker.waitForErrors()

	e.ensureTrackerIsNotWatchingKey(e.testTrackerKey)
}

// This verifies cases where the tracker watches for errors using a lower version
// after initiating a watch with a higher version for the same workspace / namespace combination
func (e *ErrorTrackerTestSuite) TestErrorTracking_OutOfOrderVersions() {
	ctx := context.Background()

	latestErrorDetails := newErrorDetails(ErrorTypeApplier, "latest error")
	latestAsyncOp, unblockLatest := e.newBlockedAsyncErrorDetails(latestErrorDetails)
	v2 := uint64(2)

	e.tracker.watchForLatestErrors(ctx, e.testWorkspace, e.testNamespace, v2, latestAsyncOp)

	oldErrorDetails := newErrorDetails(ErrorTypeKubernetes, "older error")
	oldAsyncOp, unblockOld := e.newBlockedAsyncErrorDetails(oldErrorDetails)
	v1 := uint64(1)

	e.tracker.watchForLatestErrors(ctx, e.testWorkspace, e.testNamespace, v1, oldAsyncOp)

	// at this point, verify that only entry for version=2 is being tracked
	e.ensureTrackerIsWatchingForErrors(e.testTrackerKey, v2)

	// unblock the publishing of async errors
	unblockOld()
	unblockLatest()
	e.tracker.waitForErrors()

	// ensure that only error details for the latest operation are received
	e.ensureTrackerReceivedErrorDetails(e.testTrackerKey, v2, latestErrorDetails)
}

func (e *ErrorTrackerTestSuite) TestErrorTracking_ConflictingErrorDetails() {
	ctx := context.Background()

	asyncOp1, unblockOp1 := e.newBlockedAsyncErrorDetails(e.testErrorDetails)

	v1 := uint64(1)

	e.tracker.watchForLatestErrors(ctx, e.testWorkspace, e.testNamespace, v1, asyncOp1)

	testErrorDetails2 := newErrorDetails(ErrorTypeKubernetes, "some other error")
	asyncOp2, unblockOp2 := e.newBlockedAsyncErrorDetails(testErrorDetails2)

	v2 := uint64(2)
	e.tracker.watchForLatestErrors(ctx, e.testWorkspace, e.testNamespace, v2, asyncOp2)

	// at this point, the tracker should stop tracking version=1
	e.ensureTrackerIsWatchingForErrors(e.testTrackerKey, v2)

	// unblock the publishing of async errors
	unblockOp1()
	unblockOp2()
	e.tracker.waitForErrors()

	e.ensureTrackerReceivedErrorDetails(e.testTrackerKey, v2, testErrorDetails2)
}

func (e *ErrorTrackerTestSuite) TestErrorTracking_ConflictsWithEventualSuccess() {
	ctx := context.Background()

	failingOp, unblockingFailingOp := e.newBlockedAsyncErrorDetails(e.testErrorDetails)

	v1 := uint64(1)
	e.tracker.watchForLatestErrors(ctx, e.testWorkspace, e.testNamespace, v1, failingOp)

	successfulOp, unblockSuccessfulOp := e.newBlockedAsyncErrorDetails(nil)

	v2 := uint64(2)
	e.tracker.watchForLatestErrors(ctx, e.testWorkspace, e.testNamespace, v2, successfulOp)

	// at this point, the track should stop tracking version=1
	e.ensureTrackerIsWatchingForErrors(e.testTrackerKey, v2)

	// unblock the publishing of async errors
	unblockingFailingOp()
	unblockSuccessfulOp()
	e.tracker.waitForErrors()

	e.ensureTrackerIsNotWatchingKey(e.testTrackerKey)
}

// newBlockedAsyncErrorDetails returns a blocked channel that returns the passed error details once the
// returned unblock function is invoked. The channel is closed automatically after.
func (e *ErrorTrackerTestSuite) newBlockedAsyncErrorDetails(details *ErrorDetails) (result <-chan *ErrorDetails, unblock func()) {
	var wg sync.WaitGroup

	wg.Add(1)

	return rdutil.RunWithAsyncResult[*ErrorDetails](func(ch chan<- *ErrorDetails) {
		wg.Wait()

		ch <- details
	}), wg.Done
}

func (e *ErrorTrackerTestSuite) SetupTest() {
	e.tracker = newErrorDetailsTracker()

	e.testError = errors.New("applier error")
	e.testErrorDetails = newErrorDetails(ErrorTypeApplier, e.testError.Error())
	e.testWorkspace = "test-workspace"
	e.testNamespace = "test-namespace"
	e.testTrackerKey = errorTrackerKey{
		name:      e.testWorkspace,
		namespace: e.testNamespace,
	}
}

func (e *ErrorTrackerTestSuite) ensureTrackerIsWatchingForErrors(key errorTrackerKey, version uint64) {
	snapshot := e.tracker.createSnapshot()
	e.Contains(snapshot, key)
	e.Equal(operationState{
		version:      version,
		errorDetails: nil,
	}, snapshot[key])
}

func (e *ErrorTrackerTestSuite) ensureTrackerReceivedErrorDetails(key errorTrackerKey, version uint64, details *ErrorDetails) {
	snapshot := e.tracker.createSnapshot()
	e.Contains(snapshot, key)
	e.Equal(operationState{
		version:      version,
		errorDetails: details,
	}, snapshot[key])
}

func (e *ErrorTrackerTestSuite) ensureTrackerIsNotWatchingKey(key errorTrackerKey) {
	snapshot := e.tracker.createSnapshot()
	e.NotContains(snapshot, key)
}
