package workertest

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

type matrixTestStore struct {
	mu              sync.Mutex
	nextWorker      int
	crashedWorkers  map[CrashPoint]*matrixTestWorker
	restartedWorker map[CrashPoint]*matrixTestWorker
}

type matrixTestWorker struct {
	store *matrixTestStore
	id    int
}

func TestRunDurableCriticalCrashMatrixCoversEveryBoundary(t *testing.T) {
	seen := make(map[CrashPoint]int)
	var mu sync.Mutex
	RunDurableCriticalCrashMatrix(t, time.Second, func(t *testing.T, point CrashPoint) DurableCriticalCase[*matrixTestStore, *matrixTestWorker] {
		store := &matrixTestStore{
			crashedWorkers:  make(map[CrashPoint]*matrixTestWorker),
			restartedWorker: make(map[CrashPoint]*matrixTestWorker),
		}
		return DurableCriticalCase[*matrixTestStore, *matrixTestWorker]{
			DurableStore: store,
			NewWorker: func(_ context.Context, actualStore *matrixTestStore) (*matrixTestWorker, error) {
				actualStore.mu.Lock()
				defer actualStore.mu.Unlock()
				actualStore.nextWorker++
				return &matrixTestWorker{store: actualStore, id: actualStore.nextWorker}, nil
			},
			RunUntilCrash: func(_ context.Context, worker *matrixTestWorker, probe Probe) error {
				if worker.store != store {
					return errors.New("crash worker was not bound to the matrix store")
				}
				mu.Lock()
				seen[point]++
				mu.Unlock()
				store.mu.Lock()
				store.crashedWorkers[point] = worker
				store.mu.Unlock()
				return probe(point)
			},
			RestartAndDrain: func(_ context.Context, worker *matrixTestWorker) error {
				if worker.store != store {
					return errors.New("restarted worker was not bound to the matrix store")
				}
				store.mu.Lock()
				defer store.mu.Unlock()
				store.restartedWorker[point] = worker
				return nil
			},
			AssertRecovered: func(_ context.Context, actualStore *matrixTestStore) error {
				actualStore.mu.Lock()
				defer actualStore.mu.Unlock()
				initial := actualStore.crashedWorkers[point]
				restarted := actualStore.restartedWorker[point]
				if actualStore.nextWorker != 2 || initial == nil || restarted == nil ||
					initial.id != 1 || restarted.id != 2 || initial == restarted ||
					initial.store != actualStore || restarted.store != actualStore {
					return errors.New("restart did not construct exactly two workers against the matrix store")
				}
				return nil
			},
		}
	})
	for _, point := range DurableCriticalCrashPoints() {
		if seen[point] != 1 {
			t.Fatalf("crash point %s ran %d times", point, seen[point])
		}
	}
}

func TestValidateFreshWorkerRequiresConcreteDistinctInstances(t *testing.T) {
	t.Parallel()
	type otherWorker struct{}
	initial := &matrixTestWorker{}
	for _, test := range []struct {
		name      string
		restarted any
		want      string
	}{
		{
			name:      "fresh concrete worker",
			restarted: &matrixTestWorker{},
		},
		{
			name:      "same worker",
			restarted: initial,
			want:      "reused",
		},
		{
			name:      "different worker type",
			restarted: &otherWorker{},
			want:      "type changed",
		},
		{
			name:      "non-pointer worker",
			restarted: matrixTestWorker{},
			want:      "concrete pointer",
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			err := validateFreshWorker(initial, test.restarted)
			if test.want == "" {
				if err != nil {
					t.Fatalf("valid fresh worker: %v", err)
				}
				return
			}
			if err == nil || !strings.Contains(err.Error(), test.want) {
				t.Fatalf("fresh worker error=%v, want %q", err, test.want)
			}
		})
	}
}

func TestReferenceIdentityRejectsSelfReportedOrNilFixtures(t *testing.T) {
	for _, test := range []struct {
		name  string
		value any
	}{
		{name: "self-reported string", value: "invented-store"},
		{name: "nil pointer", value: (*matrixTestStore)(nil)},
	} {
		t.Run(test.name, func(t *testing.T) {
			if _, err := referenceIdentity("fixture", test.value); err == nil {
				t.Fatalf("unbound fixture was accepted: %#v", test.value)
			}
		})
	}
}

func TestRunBeforeDeadlineDoesNotHangOnUncooperativeCallback(t *testing.T) {
	t.Parallel()
	release := make(chan struct{})
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
	defer cancel()
	value, err := runBeforeDeadline(ctx, func() string {
		<-release
		return "late"
	})
	close(release)
	if !errors.Is(err, context.DeadlineExceeded) || value != "" {
		t.Fatalf("uncooperative callback result=(%q, %v)", value, err)
	}
}

func TestRunBeforeDeadlineReturnsCompletedCallback(t *testing.T) {
	t.Parallel()
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	value, err := runBeforeDeadline(ctx, func() string { return "complete" })
	if err != nil || value != "complete" {
		t.Fatalf("completed callback result=(%q, %v)", value, err)
	}
}
