/*
 * Copyright 2026 CloudWeGo Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package adk

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

	"github.com/stretchr/testify/assert"

	"github.com/cloudwego/eino/compose"
)

func TestAgentCancelFunc_MultiCall_EscalateToImmediate(t *testing.T) {
	cc := newCancelContext()
	var interruptCalls int32
	cc.setGraphInterruptFunc(func(opts ...compose.GraphInterruptOption) {
		atomic.AddInt32(&interruptCalls, 1)
	})
	cancelFn := cc.buildCancelFunc()

	handle1, _ := cancelFn(WithAgentCancelMode(CancelAfterChatModel))
	handle2, _ := cancelFn(WithAgentCancelMode(CancelImmediate))
	assert.Equal(t, int32(1), atomic.LoadInt32(&interruptCalls))

	cancelErr := cc.createCancelError()
	assert.Equal(t, CancelImmediate, cancelErr.Info.Mode)
	assert.True(t, cancelErr.Info.Escalated)
	assert.False(t, cancelErr.Info.Timeout)

	assert.True(t, cc.markCancelHandled())
	assert.NoError(t, handle1.Wait())
	assert.NoError(t, handle2.Wait())
}

func TestAgentCancelFunc_MultiCall_JoinSafePointModes(t *testing.T) {
	cc := newCancelContext()
	cancelFn := cc.buildCancelFunc()

	handle1, _ := cancelFn(WithAgentCancelMode(CancelAfterChatModel))
	handle2, _ := cancelFn(WithAgentCancelMode(CancelAfterToolCalls))

	want := CancelAfterChatModel | CancelAfterToolCalls
	assert.Equal(t, want, cc.getMode())

	assert.True(t, cc.markCancelHandled())
	assert.NoError(t, handle1.Wait())
	assert.NoError(t, handle2.Wait())
}

func TestAgentCancelFunc_MultiCall_TimeoutDeadlineJoinUsesAbsoluteTime(t *testing.T) {
	cc := newCancelContext()
	cancelFn := cc.buildCancelFunc()

	handle1, _ := cancelFn(
		WithAgentCancelMode(CancelAfterChatModel),
		WithAgentCancelTimeout(200*time.Millisecond),
	)

	firstDeadline := cc.getDeadlineUnixNano()
	assert.NotZero(t, firstDeadline)

	time.Sleep(50 * time.Millisecond)

	handle2, _ := cancelFn(
		WithAgentCancelMode(CancelAfterToolCalls),
		WithAgentCancelTimeout(60*time.Millisecond),
	)

	secondDeadline := cc.getDeadlineUnixNano()
	assert.NotZero(t, secondDeadline)
	assert.Less(t, secondDeadline, firstDeadline)

	assert.True(t, cc.markCancelHandled())
	assert.NoError(t, handle1.Wait())
	assert.NoError(t, handle2.Wait())
}

func TestAgentCancelFunc_MultiCall_TimeoutEscalationReturnsErrCancelTimeout(t *testing.T) {
	cc := newCancelContext()
	var interruptCalls int32
	interruptCh := make(chan struct{}, 1)
	cc.setGraphInterruptFunc(func(opts ...compose.GraphInterruptOption) {
		atomic.AddInt32(&interruptCalls, 1)
		select {
		case interruptCh <- struct{}{}:
		default:
		}
	})
	cancelFn := cc.buildCancelFunc()
	handle, _ := cancelFn(
		WithAgentCancelMode(CancelAfterChatModel),
		WithAgentCancelTimeout(30*time.Millisecond),
	)

	select {
	case <-interruptCh:
	case <-time.After(1 * time.Second):
		t.Fatal("timeout escalation did not interrupt")
	}
	assert.Equal(t, int32(1), atomic.LoadInt32(&interruptCalls))

	cancelErr := cc.createCancelError()
	assert.Equal(t, CancelAfterChatModel, cancelErr.Info.Mode)
	assert.True(t, cancelErr.Info.Escalated)
	assert.True(t, cancelErr.Info.Timeout)

	assert.True(t, cc.markCancelHandled())
	assert.Equal(t, ErrCancelTimeout, handle.Wait())
}
