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/errs"
)

// permFakeRepo 内嵌 acp.Repository 接口（nil），仅实现 PermissionService 用到的方法；
// 其余方法被调用会 panic（测试不应触达）。
type permFakeRepo struct {
	acp.Repository
	mu       sync.Mutex
	reqs     map[uuid.UUID]*acp.PermissionRequest
	sessions map[uuid.UUID]*acp.Session
}

func newPermFakeRepo() *permFakeRepo {
	return &permFakeRepo{
		reqs:     map[uuid.UUID]*acp.PermissionRequest{},
		sessions: map[uuid.UUID]*acp.Session{},
	}
}

func (r *permFakeRepo) InsertPermissionRequest(_ context.Context, p *acp.PermissionRequest) (*acp.PermissionRequest, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	cp := *p
	cp.CreatedAt = time.Unix(0, 0)
	r.reqs[cp.ID] = &cp
	out := cp
	return &out, nil
}

func (r *permFakeRepo) GetPermissionRequestByID(_ context.Context, id uuid.UUID) (*acp.PermissionRequest, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	p, ok := r.reqs[id]
	if !ok {
		return nil, errs.New(errs.CodeAcpPermissionNotFound, "not found")
	}
	out := *p
	return &out, nil
}

func (r *permFakeRepo) GetSessionByID(_ context.Context, id uuid.UUID) (*acp.Session, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	s, ok := r.sessions[id]
	if !ok {
		return nil, errs.New(errs.CodeAcpSessionNotFound, "not found")
	}
	out := *s
	return &out, nil
}
func (r *permFakeRepo) GetSessionByStepID(context.Context, uuid.UUID) (*acp.Session, error) {
	return nil, errs.New(errs.CodeAcpSessionNotFound, "not found")
}
func (r *permFakeRepo) GetCrashedSessionForResume(context.Context, uuid.UUID) (*acp.Session, error) {
	return nil, errs.New(errs.CodeAcpSessionNotFound, "not found")
}
func (r *permFakeRepo) ResetSessionForResume(context.Context, uuid.UUID) error { return nil }

func (r *permFakeRepo) DecidePermissionRequest(_ context.Context, id uuid.UUID, status acp.PermissionStatus, chosen *string, decidedBy *uuid.UUID) (*acp.PermissionRequest, bool, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	p, ok := r.reqs[id]
	if !ok || p.Status != acp.PermissionPending {
		return nil, false, nil // CAS 失败：已非 pending
	}
	p.Status = status
	p.ChosenOptionID = chosen
	p.DecidedBy = decidedBy
	out := *p
	return &out, true, nil
}

func (r *permFakeRepo) ExpirePendingPermissionRequestsBySession(_ context.Context, sid uuid.UUID) (int64, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	var n int64
	for _, p := range r.reqs {
		if p.SessionID == sid && p.Status == acp.PermissionPending {
			p.Status = acp.PermissionExpired
			n++
		}
	}
	return n, nil
}

func (r *permFakeRepo) ListPendingPermissionRequestsByUser(_ context.Context, userID uuid.UUID) ([]*acp.PermissionRequest, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	var out []*acp.PermissionRequest
	for _, p := range r.reqs {
		if p.Status != acp.PermissionPending {
			continue
		}
		sess, ok := r.sessions[p.SessionID]
		if !ok || sess.UserID != userID {
			continue
		}
		cp := *p
		out = append(out, &cp)
	}
	return out, nil
}

func (r *permFakeRepo) statusOf(id uuid.UUID) acp.PermissionStatus {
	r.mu.Lock()
	defer r.mu.Unlock()
	return r.reqs[id].Status
}

func (r *permFakeRepo) onlyReqID() uuid.UUID {
	r.mu.Lock()
	defer r.mu.Unlock()
	for id := range r.reqs {
		return id
	}
	return uuid.Nil
}

func optionsAllowReject() []handlers.PermOption {
	return []handlers.PermOption{{ID: "allow_once"}, {ID: "reject_once"}}
}

func TestPermission_AutoAllow_WhenAllowlisted(t *testing.T) {
	repo := newPermFakeRepo()
	svc := acp.NewPermissionService(repo, nil, time.Minute, nil)
	sess := handlers.SessionContext{
		SessionID:     uuid.New(),
		UserID:        uuid.New(),
		ToolAllowlist: []string{"safe.tool"},
	}
	chosen := svc.Decide(context.Background(), sess, "req-1",
		[]byte(`{"name":"safe.tool"}`), optionsAllowReject())
	require.Equal(t, "allow_once", chosen)
	require.Equal(t, acp.PermissionAuto, repo.statusOf(repo.onlyReqID()))
}

func TestPermission_AutoAllow_Wildcard(t *testing.T) {
	repo := newPermFakeRepo()
	svc := acp.NewPermissionService(repo, nil, time.Minute, nil)
	sess := handlers.SessionContext{
		SessionID:     uuid.New(),
		UserID:        uuid.New(),
		ToolAllowlist: []string{"*"},
	}
	chosen := svc.Decide(context.Background(), sess, "req-1",
		[]byte(`{"name":"any.tool"}`), optionsAllowReject())
	require.Equal(t, "allow_once", chosen)
	require.Equal(t, acp.PermissionAuto, repo.statusOf(repo.onlyReqID()))
}

func TestPermission_Wildcard_UnknownToolName_Pending(t *testing.T) {
	// allowlist 含 "*" 但 toolCall 无法识别名称 → 不自动放行，走人工审批。
	repo := newPermFakeRepo()
	uid := uuid.New()
	sid := uuid.New()
	repo.sessions[sid] = &acp.Session{ID: sid, UserID: uid}
	svc := acp.NewPermissionService(repo, nil, time.Minute, nil)
	sess := handlers.SessionContext{SessionID: sid, UserID: uid, ToolAllowlist: []string{"*"}}

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

	var reqID uuid.UUID
	require.Eventually(t, func() bool {
		reqID = repo.onlyReqID()
		return reqID != uuid.Nil && repo.statusOf(reqID) == acp.PermissionPending
	}, time.Second, 5*time.Millisecond)

	require.NoError(t, svc.Resolve(context.Background(), acp.Caller{UserID: uid}, reqID, true, "allow_once"))
	select {
	case chosen := <-resCh:
		require.Equal(t, "allow_once", chosen)
	case <-time.After(time.Second):
		t.Fatal("Decide 未在批准后返回")
	}
	require.Equal(t, acp.PermissionApproved, repo.statusOf(reqID))
}

func TestPermission_Pending_ThenApprove(t *testing.T) {
	repo := newPermFakeRepo()
	uid := uuid.New()
	sid := uuid.New()
	repo.sessions[sid] = &acp.Session{ID: sid, UserID: uid}
	svc := acp.NewPermissionService(repo, nil, time.Minute, nil)
	sess := handlers.SessionContext{SessionID: sid, UserID: uid} // 无白名单 → 挂起

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

	// 等待 pending 落库
	var reqID uuid.UUID
	require.Eventually(t, func() bool {
		reqID = repo.onlyReqID()
		return reqID != uuid.Nil && repo.statusOf(reqID) == acp.PermissionPending
	}, time.Second, 5*time.Millisecond)

	require.NoError(t, svc.Resolve(context.Background(), acp.Caller{UserID: uid}, reqID, true, "allow_once"))
	select {
	case chosen := <-resCh:
		require.Equal(t, "allow_once", chosen)
	case <-time.After(time.Second):
		t.Fatal("Decide 未在批准后返回")
	}
	require.Equal(t, acp.PermissionApproved, repo.statusOf(reqID))
}

func TestPermission_Pending_ThenDeny(t *testing.T) {
	repo := newPermFakeRepo()
	uid := uuid.New()
	sid := uuid.New()
	repo.sessions[sid] = &acp.Session{ID: sid, UserID: uid}
	svc := acp.NewPermissionService(repo, nil, time.Minute, nil)
	sess := handlers.SessionContext{SessionID: sid, UserID: uid}

	resCh := make(chan string, 1)
	go func() {
		resCh <- svc.Decide(context.Background(), sess, "req-1",
			[]byte(`{"name":"danger.tool"}`), optionsAllowReject())
	}()
	var reqID uuid.UUID
	require.Eventually(t, func() bool {
		reqID = repo.onlyReqID()
		return reqID != uuid.Nil && repo.statusOf(reqID) == acp.PermissionPending
	}, time.Second, 5*time.Millisecond)

	require.NoError(t, svc.Resolve(context.Background(), acp.Caller{UserID: uid}, reqID, false, ""))
	require.Equal(t, "reject_once", <-resCh)
	require.Equal(t, acp.PermissionDenied, repo.statusOf(reqID))
}

func TestPermission_Timeout_DefaultsToReject(t *testing.T) {
	repo := newPermFakeRepo()
	uid := uuid.New()
	sid := uuid.New()
	repo.sessions[sid] = &acp.Session{ID: sid, UserID: uid}
	svc := acp.NewPermissionService(repo, nil, 30*time.Millisecond, nil)
	sess := handlers.SessionContext{SessionID: sid, UserID: uid}

	chosen := svc.Decide(context.Background(), sess, "req-1",
		[]byte(`{"name":"danger.tool"}`), optionsAllowReject())
	require.Equal(t, "", chosen) // 默认拒绝
	require.Equal(t, acp.PermissionExpired, repo.statusOf(repo.onlyReqID()))
}

func TestPermission_DuplicateDecide_Conflict(t *testing.T) {
	repo := newPermFakeRepo()
	uid := uuid.New()
	sid := uuid.New()
	repo.sessions[sid] = &acp.Session{ID: sid, UserID: uid}
	svc := acp.NewPermissionService(repo, nil, time.Minute, nil)
	sess := handlers.SessionContext{SessionID: sid, UserID: uid}

	go svc.Decide(context.Background(), sess, "req-1",
		[]byte(`{"name":"danger.tool"}`), optionsAllowReject())
	var reqID uuid.UUID
	require.Eventually(t, func() bool {
		reqID = repo.onlyReqID()
		return reqID != uuid.Nil && repo.statusOf(reqID) == acp.PermissionPending
	}, time.Second, 5*time.Millisecond)

	require.NoError(t, svc.Resolve(context.Background(), acp.Caller{UserID: uid}, reqID, true, "allow_once"))
	err := svc.Resolve(context.Background(), acp.Caller{UserID: uid}, reqID, true, "allow_once")
	ae, ok := errs.As(err)
	require.True(t, ok)
	require.Equal(t, errs.CodeAcpPermissionAlreadyDecided, ae.Code)
}

func TestPermission_Resolve_NotOwner_Forbidden(t *testing.T) {
	repo := newPermFakeRepo()
	owner := uuid.New()
	sid := uuid.New()
	repo.sessions[sid] = &acp.Session{ID: sid, UserID: owner}
	svc := acp.NewPermissionService(repo, nil, time.Minute, nil)
	sess := handlers.SessionContext{SessionID: sid, UserID: owner}

	go svc.Decide(context.Background(), sess, "req-1",
		[]byte(`{"name":"danger.tool"}`), optionsAllowReject())
	var reqID uuid.UUID
	require.Eventually(t, func() bool {
		reqID = repo.onlyReqID()
		return reqID != uuid.Nil && repo.statusOf(reqID) == acp.PermissionPending
	}, time.Second, 5*time.Millisecond)

	err := svc.Resolve(context.Background(), acp.Caller{UserID: uuid.New()}, reqID, true, "allow_once")
	ae, ok := errs.As(err)
	require.True(t, ok)
	require.Equal(t, errs.CodeAcpSessionNotOwned, ae.Code)
	// admin 可处理任意会话
	require.NoError(t, svc.Resolve(context.Background(), acp.Caller{UserID: uuid.New(), IsAdmin: true}, reqID, true, "allow_once"))
}

func TestPermission_CancelSession_UnblocksDecide(t *testing.T) {
	repo := newPermFakeRepo()
	uid := uuid.New()
	sid := uuid.New()
	repo.sessions[sid] = &acp.Session{ID: sid, UserID: uid}
	svc := acp.NewPermissionService(repo, nil, time.Minute, nil)
	sess := handlers.SessionContext{SessionID: sid, UserID: uid}

	resCh := make(chan string, 1)
	go func() {
		resCh <- svc.Decide(context.Background(), sess, "req-1",
			[]byte(`{"name":"danger.tool"}`), optionsAllowReject())
	}()
	require.Eventually(t, func() bool {
		id := repo.onlyReqID()
		return id != uuid.Nil && repo.statusOf(id) == acp.PermissionPending
	}, time.Second, 5*time.Millisecond)

	svc.CancelSession(context.Background(), sid)
	select {
	case chosen := <-resCh:
		require.Equal(t, "", chosen)
	case <-time.After(time.Second):
		t.Fatal("CancelSession 未解除 Decide 阻塞")
	}
	require.Equal(t, acp.PermissionExpired, repo.statusOf(repo.onlyReqID()))
}

// fakeMaintainer 满足 acp.ProjectMaintainer：仅当 projectID 命中 allow 集合时放行。
type fakeMaintainer struct {
	allow map[uuid.UUID]bool
}

func (f fakeMaintainer) CanWriteProject(_ context.Context, callerID uuid.UUID, isAdmin bool, projectID uuid.UUID) (bool, error) {
	if isAdmin {
		return true, nil
	}
	return f.allow[projectID], nil
}

// 注入 ProjectMaintainer 后：会话所属 project 的任意 maintainer（非会话 owner）
// 也可处理待审权限请求（autonomy roadmap §11）。
func TestPermission_Resolve_ProjectMaintainer_Allowed(t *testing.T) {
	repo := newPermFakeRepo()
	owner := uuid.New()
	maintainer := uuid.New()
	pid := uuid.New()
	sid := uuid.New()
	repo.sessions[sid] = &acp.Session{ID: sid, UserID: owner, ProjectID: pid}
	svc := acp.NewPermissionService(repo, nil, time.Minute, nil)
	svc.SetProjectMaintainer(fakeMaintainer{allow: map[uuid.UUID]bool{pid: true}})
	sess := handlers.SessionContext{SessionID: sid, UserID: owner}

	go svc.Decide(context.Background(), sess, "req-1",
		[]byte(`{"name":"danger.tool"}`), optionsAllowReject())
	var reqID uuid.UUID
	require.Eventually(t, func() bool {
		reqID = repo.onlyReqID()
		return reqID != uuid.Nil && repo.statusOf(reqID) == acp.PermissionPending
	}, time.Second, 5*time.Millisecond)

	// 非 owner 但是 project maintainer → 可处理。
	require.NoError(t, svc.Resolve(context.Background(), acp.Caller{UserID: maintainer}, reqID, true, "allow_once"))
	require.Equal(t, acp.PermissionApproved, repo.statusOf(reqID))
}

// 非 maintainer 的外部用户仍被拒（fail-closed）；项目不在 allow 集合时也拒。
func TestPermission_Resolve_NonMaintainer_Forbidden(t *testing.T) {
	repo := newPermFakeRepo()
	owner := uuid.New()
	pid := uuid.New()
	otherPid := uuid.New()
	sid := uuid.New()
	repo.sessions[sid] = &acp.Session{ID: sid, UserID: owner, ProjectID: pid}
	svc := acp.NewPermissionService(repo, nil, time.Minute, nil)
	// maintainer 仅对 otherPid 有权，而会话属于 pid。
	svc.SetProjectMaintainer(fakeMaintainer{allow: map[uuid.UUID]bool{otherPid: true}})
	sess := handlers.SessionContext{SessionID: sid, UserID: owner}

	go svc.Decide(context.Background(), sess, "req-1",
		[]byte(`{"name":"danger.tool"}`), optionsAllowReject())
	var reqID uuid.UUID
	require.Eventually(t, func() bool {
		reqID = repo.onlyReqID()
		return reqID != uuid.Nil && repo.statusOf(reqID) == acp.PermissionPending
	}, time.Second, 5*time.Millisecond)

	err := svc.Resolve(context.Background(), acp.Caller{UserID: uuid.New()}, reqID, true, "allow_once")
	ae, ok := errs.As(err)
	require.True(t, ok)
	require.Equal(t, errs.CodeAcpSessionNotOwned, ae.Code)
}
