package hook

import (
	"context"
	"testing"

	"github.com/shiningrush/fastflow/pkg/entity"
	"github.com/stretchr/testify/require"
)

var testOwnedDagIDs = map[string]struct{}{"owned-dag": {}}

func ownedDagIns() *entity.DagInstance {
	di := &entity.DagInstance{DagID: "owned-dag"}
	di.ID = "test-dag-ins"
	return di
}

// withSavedBeforeFail saves and restores the process-global BeforeFail hook so
// tests that register through chainBeforeFail don't leak into each other.
func withSavedBeforeFail(t *testing.T) {
	t.Helper()
	saved := entity.HookDagInstance.BeforeFail
	t.Cleanup(func() { entity.HookDagInstance.BeforeFail = saved })
	entity.HookDagInstance.BeforeFail = nil
}

func TestChainBeforeFail_InvokesPrevThenHandle(t *testing.T) {
	withSavedBeforeFail(t)

	var order []string
	entity.HookDagInstance.BeforeFail = func(*entity.DagInstance) { order = append(order, "prev") }
	chainBeforeFail("test-hook", testOwnedDagIDs,
		func(context.Context, *entity.DagInstance) { order = append(order, "handle") })

	entity.HookDagInstance.BeforeFail(ownedDagIns())

	require.Equal(t, []string{"prev", "handle"}, order)
}

// Centralized routing: a DAG not in this hook's allowlist must not reach its
// handle. This is the filter that used to live at the top of every Handle.
func TestChainBeforeFail_NonOwnedDag_SkipsHandle(t *testing.T) {
	withSavedBeforeFail(t)

	handleCalled := false
	chainBeforeFail("test-hook", testOwnedDagIDs,
		func(context.Context, *entity.DagInstance) { handleCalled = true })

	entity.HookDagInstance.BeforeFail(&entity.DagInstance{DagID: "some-other-dag"})

	require.False(t, handleCalled, "handle must not run for a DAG outside the allowlist")
}

func TestChainBeforeFail_NilDagIns_SkipsHandle(t *testing.T) {
	withSavedBeforeFail(t)

	handleCalled := false
	chainBeforeFail("test-hook", testOwnedDagIDs,
		func(context.Context, *entity.DagInstance) { handleCalled = true })

	require.NotPanics(t, func() { entity.HookDagInstance.BeforeFail(nil) })
	require.False(t, handleCalled, "handle must not run for a nil dag instance")
}

func TestChainBeforeFail_RecoversHandlePanic(t *testing.T) {
	withSavedBeforeFail(t)

	prevCalled := false
	entity.HookDagInstance.BeforeFail = func(*entity.DagInstance) { prevCalled = true }
	chainBeforeFail("test-hook", testOwnedDagIDs,
		func(context.Context, *entity.DagInstance) { panic("boom") })

	// Must not propagate the panic — fastflow's executor would otherwise die.
	require.NotPanics(t, func() { entity.HookDagInstance.BeforeFail(ownedDagIns()) })
	require.True(t, prevCalled, "prev hook should still run before the panicking handle")
}

func TestChainBeforeFail_PrevPanic_IsIsolated_HandleStillRuns(t *testing.T) {
	withSavedBeforeFail(t)

	entity.HookDagInstance.BeforeFail = func(*entity.DagInstance) { panic("prev boom") }
	handleCalled := false
	chainBeforeFail("test-hook", testOwnedDagIDs,
		func(context.Context, *entity.DagInstance) { handleCalled = true })

	// A panic in an earlier hook must be contained AND must not prevent this
	// hook's handle from running — that fault isolation is the point of the chain.
	require.NotPanics(t, func() { entity.HookDagInstance.BeforeFail(ownedDagIns()) })
	require.True(t, handleCalled, "handle must still run even though an earlier hook panicked")
}
