package workertest

import (
	"errors"
	"fmt"
	"sync/atomic"
)

var ErrInjectedCrash = errors.New("worker conformance: injected process crash")

// CrashPoint is a stable transition boundary shared by durable-critical
// conformance tests. Domain fixtures may add narrower points, but must not
// rename these boundaries and accidentally omit part of the common matrix.
type CrashPoint string

const (
	BeforeAcceptance  CrashPoint = "before-acceptance"
	AfterAcceptance   CrashPoint = "after-acceptance"
	AfterClaim        CrashPoint = "after-claim"
	BeforeEffect      CrashPoint = "before-effect"
	AfterEffect       CrashPoint = "after-effect"
	BeforeDisposition CrashPoint = "before-disposition"
	AfterDisposition  CrashPoint = "after-disposition"
	DuringHeartbeat   CrashPoint = "during-heartbeat"
	LeaseExpired      CrashPoint = "lease-expired"
	OwnershipTransfer CrashPoint = "ownership-transfer"
)

var durableCriticalCrashPoints = []CrashPoint{
	BeforeAcceptance,
	AfterAcceptance,
	AfterClaim,
	BeforeEffect,
	AfterEffect,
	BeforeDisposition,
	AfterDisposition,
	DuringHeartbeat,
	LeaseExpired,
	OwnershipTransfer,
}

func DurableCriticalCrashPoints() []CrashPoint {
	return append([]CrashPoint(nil), durableCriticalCrashPoints...)
}

// Injector fails exactly once at the selected boundary. Returning a sentinel
// lets a domain fixture abandon the current worker instance without running
// normal disposition, then construct a fresh instance against the same store.
// The fixture, not Injector, remains responsible for proving persisted state.
type Injector struct {
	point     CrashPoint
	triggered atomic.Bool
}

func NewInjector(point CrashPoint) (*Injector, error) {
	if !isKnownCrashPoint(point) {
		return nil, fmt.Errorf("worker conformance: unknown crash point %q", point)
	}
	return &Injector{point: point}, nil
}

func (i *Injector) Probe(point CrashPoint) error {
	if i == nil || point != i.point || !i.triggered.CompareAndSwap(false, true) {
		return nil
	}
	return fmt.Errorf("%w at %s", ErrInjectedCrash, point)
}

func (i *Injector) Triggered() bool { return i != nil && i.triggered.Load() }

func isKnownCrashPoint(point CrashPoint) bool {
	for _, candidate := range durableCriticalCrashPoints {
		if point == candidate {
			return true
		}
	}
	return false
}
