package runtimeworker

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

	"github.com/nexuscrm/files"
	"github.com/nexuscrm/persistence"
)

type fileUploadLifecycleStoreStub struct {
	readyErr error
	hintErr  error
	due      bool
	count    int64
	err      error

	hintCalls    atomic.Int64
	processCalls atomic.Int64
}

func (s *fileUploadLifecycleStoreStub) CheckReady(context.Context) error {
	return s.readyErr
}

func (s *fileUploadLifecycleStoreStub) HasExpired(context.Context) (bool, error) {
	s.hintCalls.Add(1)
	return s.due, s.hintErr
}

func (s *fileUploadLifecycleStoreStub) ProcessExpired(context.Context, int, time.Duration) (int64, error) {
	s.processCalls.Add(1)
	return s.count, s.err
}

type fileUploadLifecycleObservationResult struct {
	state files.RuntimeUploadLifecycleState
	err   error
}

type fileUploadLifecycleObservationSourceStub struct {
	mu      sync.Mutex
	results []fileUploadLifecycleObservationResult
	calls   int
}

func (s *fileUploadLifecycleObservationSourceStub) Observe(context.Context) (files.RuntimeUploadLifecycleState, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.calls++
	if len(s.results) == 0 {
		return files.RuntimeUploadLifecycleState{}, nil
	}
	result := s.results[0]
	if len(s.results) > 1 {
		s.results = s.results[1:]
	}
	return result.state, result.err
}

type blockingFileUploadLifecycleObservationSource struct {
	started chan struct{}
	once    sync.Once
}

func (s *blockingFileUploadLifecycleObservationSource) Observe(ctx context.Context) (files.RuntimeUploadLifecycleState, error) {
	s.once.Do(func() { close(s.started) })
	<-ctx.Done()
	return files.RuntimeUploadLifecycleState{}, ctx.Err()
}

func testFileUploadLifecycleObservationFactory(source fileUploadLifecycleObservationSource) fileUploadLifecycleObservationFactory {
	return func() (fileUploadLifecycleObservationSource, func() error, error) {
		return source, func() error { return nil }, nil
	}
}

func newTestFileUploadLifecycleWorker(
	t *testing.T,
	store fileUploadLifecycleStore,
	source fileUploadLifecycleObservationSource,
) *fileUploadLifecycleWorker {
	t.Helper()
	worker, err := newFileUploadLifecycleWorker(
		store, 10, time.Second, time.Minute, 0,
		testFileUploadLifecycleObservationFactory(source),
	)
	if err != nil {
		t.Fatal(err)
	}
	return worker
}

func TestFileUploadLifecycleWorkerSeparatesBusinessProgressFromExactDiagnostics(t *testing.T) {
	store := &fileUploadLifecycleStoreStub{due: true, count: 3}
	source := &fileUploadLifecycleObservationSourceStub{results: []fileUploadLifecycleObservationResult{{
		state: files.RuntimeUploadLifecycleState{
			QueueDepth: 4, PoisonDepth: 2, OldestAge: 3 * time.Second,
		},
	}}}
	worker := newTestFileUploadLifecycleWorker(t, store, source)
	if err := worker.Prepare(t.Context()); err != nil {
		t.Fatal(err)
	}
	initial := worker.snapshot()
	if !initial.ObservationEnabled || !initial.ObservationStale || !initial.ObservationLastSuccess.IsZero() {
		t.Fatalf("initial snapshot = %#v, want unknown/stale diagnostics", initial)
	}
	if err := worker.runPass(t.Context()); err != nil {
		t.Fatal(err)
	}
	if source.calls != 0 {
		t.Fatalf("business pass exact observation calls = %d, want zero", source.calls)
	}
	worker.refreshObservationWithTimeout(t.Context(), time.Second)
	snapshot := worker.snapshot()
	if snapshot.QueueDepth != 4 || snapshot.PoisonDepth != 2 ||
		snapshot.OldestAge != 3*time.Second || snapshot.Processed != 3 ||
		snapshot.Attempts != 1 || snapshot.Failures != 0 || snapshot.InFlight != 0 ||
		snapshot.ObservationStale || snapshot.ObservationLastSuccess.IsZero() || snapshot.OldestObservedAt.IsZero() {
		t.Fatalf("snapshot = %#v", snapshot)
	}
}

func TestFileUploadLifecycleWorkerTreatsOwnershipLossAsProcessFatal(t *testing.T) {
	store := &fileUploadLifecycleStoreStub{due: true, err: persistence.ErrRuntimeWorkOwnershipLost}
	worker := newTestFileUploadLifecycleWorker(t, store, &fileUploadLifecycleObservationSourceStub{})
	if err := worker.Run(t.Context()); !errors.Is(err, persistence.ErrRuntimeWorkOwnershipLost) {
		t.Fatalf("Run() error = %v", err)
	}
	snapshot := worker.snapshot()
	if snapshot.Attempts != 1 || snapshot.Failures != 1 {
		t.Fatalf("snapshot = %#v", snapshot)
	}
}

func TestFileUploadLifecycleWorkerSuccessfulZeroHintSkipsFencedClaim(t *testing.T) {
	store := &fileUploadLifecycleStoreStub{}
	worker := newTestFileUploadLifecycleWorker(t, store, &fileUploadLifecycleObservationSourceStub{})
	if err := worker.runPass(t.Context()); err != nil {
		t.Fatal(err)
	}
	if store.processCalls.Load() != 0 || worker.snapshot().Attempts != 0 {
		t.Fatalf("process calls=%d snapshot=%+v, want no fenced claim", store.processCalls.Load(), worker.snapshot())
	}
	if loop := worker.snapshot().WorkLoop; loop.PollChecks != 1 || loop.ConfirmedIdle != 1 || loop.ClaimAttempts != 0 {
		t.Fatalf("idle work loop=%+v", loop)
	}
}

func TestFileUploadLifecycleWorkerHintFailureFallsThroughToFencedClaim(t *testing.T) {
	store := &fileUploadLifecycleStoreStub{hintErr: errors.New("hint unavailable"), count: 1}
	worker := newTestFileUploadLifecycleWorker(t, store, &fileUploadLifecycleObservationSourceStub{})
	if err := worker.runPass(t.Context()); err != nil {
		t.Fatal(err)
	}
	if store.processCalls.Load() != 1 || worker.snapshot().Attempts != 1 || worker.snapshot().Processed != 1 {
		t.Fatalf("process calls=%d snapshot=%+v, want authoritative fallback", store.processCalls.Load(), worker.snapshot())
	}
	if loop := worker.snapshot().WorkLoop; loop.PollChecks != 1 || loop.HintFailures != 1 || loop.ClaimAttempts != 1 || loop.Claimed != 1 {
		t.Fatalf("fallback work loop=%+v", loop)
	}
}

func TestFileUploadLifecycleWorkerDoesNotObserveAtStartup(t *testing.T) {
	var opens atomic.Int64
	worker, err := newFileUploadLifecycleWorker(
		&fileUploadLifecycleStoreStub{}, 1, time.Second, time.Minute, 0,
		func() (fileUploadLifecycleObservationSource, func() error, error) {
			opens.Add(1)
			return &fileUploadLifecycleObservationSourceStub{}, func() error { return nil }, nil
		},
	)
	if err != nil {
		t.Fatal(err)
	}
	stop := worker.startObservationLoopWithTiming(t.Context(), time.Hour, time.Second)
	stop()
	if opens.Load() != 0 {
		t.Fatalf("startup observation opens=%d, want zero", opens.Load())
	}
}

func TestFileUploadLifecycleWorkerRetainsLastGoodObservationOnFailure(t *testing.T) {
	source := &fileUploadLifecycleObservationSourceStub{results: []fileUploadLifecycleObservationResult{
		{state: files.RuntimeUploadLifecycleState{QueueDepth: 7, PoisonDepth: 2, OldestAge: 3 * time.Second}},
		{err: errors.New("observation unavailable")},
	}}
	var releases atomic.Int64
	worker, err := newFileUploadLifecycleWorker(
		&fileUploadLifecycleStoreStub{}, 1, time.Second, time.Minute, 0,
		func() (fileUploadLifecycleObservationSource, func() error, error) {
			return source, func() error { releases.Add(1); return nil }, nil
		},
	)
	if err != nil {
		t.Fatal(err)
	}
	worker.refreshObservationWithTimeout(t.Context(), time.Second)
	good := worker.snapshot()
	worker.refreshObservationWithTimeout(t.Context(), time.Second)
	failed := worker.snapshot()
	if failed.QueueDepth != good.QueueDepth || failed.PoisonDepth != good.PoisonDepth || failed.OldestAge != good.OldestAge ||
		!failed.ObservationStale || failed.ObservationFailures != 1 ||
		!failed.ObservationLastSuccess.Equal(good.ObservationLastSuccess) || !failed.OldestObservedAt.Equal(good.OldestObservedAt) {
		t.Fatalf("failed snapshot=%+v, want last good %+v", failed, good)
	}
	if err := worker.closeObservation(); err != nil {
		t.Fatal(err)
	}
	if releases.Load() != 1 {
		t.Fatalf("observation releases=%d, want one", releases.Load())
	}
}

func TestFileUploadLifecycleObservationTimeoutIsSamplerOwned(t *testing.T) {
	source := &blockingFileUploadLifecycleObservationSource{started: make(chan struct{})}
	worker := newTestFileUploadLifecycleWorker(t, &fileUploadLifecycleStoreStub{}, source)
	started := time.Now()
	worker.refreshObservationWithTimeout(t.Context(), 10*time.Millisecond)
	select {
	case <-source.started:
	default:
		t.Fatal("blocking observation did not start")
	}
	if elapsed := time.Since(started); elapsed > time.Second {
		t.Fatalf("observation timeout took %s, want bounded sampler delay", elapsed)
	}
	snapshot := worker.snapshot()
	if snapshot.ObservationFailures != 1 || !snapshot.ObservationStale ||
		snapshot.ObservationDuration < 10*time.Millisecond {
		t.Fatalf("timeout snapshot=%+v", snapshot)
	}
	if err := worker.closeObservation(); err != nil {
		t.Fatal(err)
	}
}

func TestFileUploadLifecycleBlockedObservationDoesNotBlockBusinessPass(t *testing.T) {
	source := &blockingFileUploadLifecycleObservationSource{started: make(chan struct{})}
	store := &fileUploadLifecycleStoreStub{due: true, count: 1}
	worker := newTestFileUploadLifecycleWorker(t, store, source)
	observationCtx, cancelObservation := context.WithCancel(t.Context())
	observationDone := make(chan struct{})
	go func() {
		defer close(observationDone)
		worker.refreshObservationWithTimeout(observationCtx, time.Hour)
	}()
	select {
	case <-source.started:
	case <-time.After(time.Second):
		t.Fatal("observation did not start")
	}
	businessDone := make(chan error, 1)
	go func() { businessDone <- worker.runPass(t.Context()) }()
	select {
	case err := <-businessDone:
		if err != nil {
			t.Fatal(err)
		}
	case <-time.After(time.Second):
		t.Fatal("independent exact observation blocked business cleanup")
	}
	cancelObservation()
	<-observationDone
	if store.processCalls.Load() != 1 || worker.snapshot().Processed != 1 {
		t.Fatalf("process calls=%d snapshot=%+v", store.processCalls.Load(), worker.snapshot())
	}
}

func TestFileUploadLifecycleWorkerRejectsIncompleteConfiguration(t *testing.T) {
	factory := testFileUploadLifecycleObservationFactory(&fileUploadLifecycleObservationSourceStub{})
	if _, err := newFileUploadLifecycleWorker(nil, 1, time.Second, time.Minute, 0, factory); err == nil {
		t.Fatal("nil lifecycle store was accepted")
	}
	if _, err := newFileUploadLifecycleWorker(&fileUploadLifecycleStoreStub{}, 0, time.Second, time.Minute, 0, factory); err == nil {
		t.Fatal("zero batch was accepted")
	}
	if _, err := newFileUploadLifecycleWorker(&fileUploadLifecycleStoreStub{}, 1, time.Second, 0, 0, factory); err == nil {
		t.Fatal("zero lease was accepted")
	}
	if _, err := newFileUploadLifecycleWorker(&fileUploadLifecycleStoreStub{}, 1, time.Second, time.Minute, 0, nil); err == nil {
		t.Fatal("nil observation factory was accepted")
	}
}
