package acp_test

import (
	"context"
	"sync"
	"testing"
	"time"

	"github.com/google/uuid"
	"github.com/stretchr/testify/require"

	"github.com/yan1h/agent-coding-workflow/internal/acp"
	"github.com/yan1h/agent-coding-workflow/internal/acp/handlers"
	"github.com/yan1h/agent-coding-workflow/internal/infra/notify"
)

// spyNotifier captures Dispatch calls for the HITL pending path.
type spyNotifier struct {
	mu   sync.Mutex
	msgs []notify.Message
}

func (s *spyNotifier) Dispatch(_ context.Context, m notify.Message) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.msgs = append(s.msgs, m)
	return nil
}
func (s *spyNotifier) count() int {
	s.mu.Lock()
	defer s.mu.Unlock()
	return len(s.msgs)
}
func (s *spyNotifier) first() notify.Message {
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.msgs[0]
}

// spyMetrics captures permission decision outcomes.
type spyMetrics struct {
	mu       sync.Mutex
	outcomes []string
}

func (s *spyMetrics) RecordPermissionDecision(outcome string) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.outcomes = append(s.outcomes, outcome)
}
func (s *spyMetrics) snapshot() []string {
	s.mu.Lock()
	defer s.mu.Unlock()
	out := make([]string, len(s.outcomes))
	copy(out, s.outcomes)
	return out
}

func contains(ss []string, want string) bool {
	for _, s := range ss {
		if s == want {
			return true
		}
	}
	return false
}

func TestPermission_Pending_DispatchesNotifyBeforeBlocking(t *testing.T) {
	repo := newPermFakeRepo()
	uid := uuid.New()
	sid := uuid.New()
	repo.sessions[sid] = &acp.Session{ID: sid, UserID: uid}

	spy := &spyNotifier{}
	met := &spyMetrics{}
	svc := acp.NewPermissionService(repo, nil, time.Minute, nil)
	svc.SetNotifier(spy)
	svc.SetMetrics(met)

	sess := handlers.SessionContext{SessionID: sid, UserID: uid} // no allowlist -> pending

	resCh := make(chan string, 1)
	go func() {
		resCh <- svc.Decide(context.Background(), sess, "req-1",
			[]byte(`{"name":"danger.tool"}`), optionsAllowReject())
	}()

	// Notification must fire while Decide is still blocked (before resolve).
	require.Eventually(t, func() bool { return spy.count() == 1 }, time.Second, 5*time.Millisecond)
	m := spy.first()
	require.Equal(t, "acp.permission_pending", m.Topic)
	require.Equal(t, uid, m.UserID)
	require.Equal(t, notify.SeverityWarning, m.Severity)
	require.Equal(t, "/approvals", m.Link)
	require.Equal(t, sid.String(), m.Metadata["session_id"])
	require.Equal(t, "danger.tool", m.Metadata["tool_name"])

	// Resolve so Decide unblocks; exactly one dispatch (not on resolve).
	var reqID uuid.UUID
	require.Eventually(t, func() bool {
		reqID = repo.onlyReqID()
		return reqID != uuid.Nil
	}, time.Second, 5*time.Millisecond)
	require.NoError(t, svc.Resolve(context.Background(), acp.Caller{UserID: uid}, reqID, true, "allow_once"))
	<-resCh
	require.Equal(t, 1, spy.count(), "dispatch must fire once (pending), not on resolve")

	// Metrics: approved recorded.
	require.True(t, contains(met.snapshot(), "approved"))
}

func TestPermission_Metrics_AutoAndExpired(t *testing.T) {
	repo := newPermFakeRepo()
	uid := uuid.New()
	sid := uuid.New()
	repo.sessions[sid] = &acp.Session{ID: sid, UserID: uid}

	met := &spyMetrics{}
	svc := acp.NewPermissionService(repo, nil, 30*time.Millisecond, nil)
	svc.SetMetrics(met)

	// auto path
	svc.Decide(context.Background(), handlers.SessionContext{
		SessionID: sid, UserID: uid, ToolAllowlist: []string{"*"},
	}, "req-auto", []byte(`{"name":"safe.tool"}`), optionsAllowReject())
	require.True(t, contains(met.snapshot(), "auto"))

	// expired path (no allowlist, times out)
	chosen := svc.Decide(context.Background(), handlers.SessionContext{
		SessionID: sid, UserID: uid,
	}, "req-exp", []byte(`{"name":"danger.tool"}`), optionsAllowReject())
	require.Equal(t, "", chosen)
	require.True(t, contains(met.snapshot(), "expired"))
}

func TestPermission_ListPendingForUser(t *testing.T) {
	repo := newPermFakeRepo()
	uid := uuid.New()
	other := uuid.New()
	sidA := uuid.New()
	sidB := uuid.New()
	repo.sessions[sidA] = &acp.Session{ID: sidA, UserID: uid}
	repo.sessions[sidB] = &acp.Session{ID: sidB, UserID: other}
	// Two pending for uid (across sessions), one for other.
	repo.reqs[uuid.New()] = &acp.PermissionRequest{ID: uuid.New(), SessionID: sidA, Status: acp.PermissionPending}
	repo.reqs[uuid.New()] = &acp.PermissionRequest{ID: uuid.New(), SessionID: sidA, Status: acp.PermissionPending}
	repo.reqs[uuid.New()] = &acp.PermissionRequest{ID: uuid.New(), SessionID: sidB, Status: acp.PermissionPending}

	svc := acp.NewPermissionService(repo, nil, time.Minute, nil)
	got, err := svc.ListPendingForUser(context.Background(), acp.Caller{UserID: uid})
	require.NoError(t, err)
	require.Len(t, got, 2)
}
