//go:build linux || darwin

package dispatcher

import (
	"os"
	"os/exec"
	"path/filepath"
	"reflect"
	"syscall"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/desktop/internal/agent"
	"github.com/tidbcloud/AgentTeam/desktop/internal/agent/runtimeproc"
	agentteamexec "github.com/tidbcloud/AgentTeam/desktop/internal/api/exec"
)

func TestProcessExitRevokesExactExecAuthorizationBeforeRegistryPersistence(t *testing.T) {
	registryPath := filepath.Join(t.TempDir(), "runtime-processes.fifo")
	// Construct the registry while its path is absent so initial loading does
	// not open the FIFO. UnregisterPID will later block writing until this test
	// admits a reader.
	registry := runtimeproc.NewRegistry(registryPath)
	require.NoError(t, syscall.Mkfifo(registryPath, 0o600))

	d := &Dispatcher{
		execAuthorizations:     &agentteamexec.RuntimeAuthorizationStore{},
		runtimeProcessRegistry: registry,
	}
	d.initServices()
	revoked := make(chan string, 1)
	d.authorizationRevoke = func(authorizationID string) <-chan struct{} {
		revoked <- authorizationID
		return closedExecDrain()
	}

	exiting := newExecAuthorizationTestProcess(t, "departed-authorization")
	processValue := reflect.ValueOf(exiting).Elem()
	runtimeBinding := processValue.FieldByName("processRuntimeBinding")
	setUnexportedValueForOwnershipTest(
		t,
		runtimeBinding.FieldByName("cmd"),
		&exec.Cmd{Process: &os.Process{Pid: 424242}},
	)
	replacement := &agent.Process{AgentID: "agent-1", SessionID: "replacement"}
	session := &interactiveSession{
		sessionKey: "agent-1", agentID: "agent-1", process: replacement,
		ephemeral: true, durableDelivery: false,
	}

	done := make(chan struct{})
	go func() {
		d.handleProcessExit(session, exiting)
		close(done)
	}()

	select {
	case authorizationID := <-revoked:
		require.Equal(t, "departed-authorization", authorizationID)
	case <-time.After(time.Second):
		// If registry persistence incorrectly precedes revoke, release its FIFO
		// writer before failing so the callback goroutine cannot leak.
		reader, openErr := os.OpenFile(registryPath, os.O_RDONLY|syscall.O_NONBLOCK, 0)
		require.NoError(t, openErr)
		<-done
		require.NoError(t, reader.Close())
		t.Fatal("Process exit waited on registry persistence before revoking its Exec authorization")
	}
	select {
	case <-done:
		t.Fatal("Process exit unexpectedly crossed the blocked registry persistence")
	default:
	}

	reader, err := os.OpenFile(registryPath, os.O_RDONLY|syscall.O_NONBLOCK, 0)
	require.NoError(t, err)
	select {
	case <-done:
	case <-time.After(time.Second):
		_ = reader.Close()
		t.Fatal("Process exit did not finish after registry persistence was released")
	}
	require.NoError(t, reader.Close())
	require.Same(t, replacement, session.process, "stale exit must preserve the replacement Process")
}
