package project

import (
	"context"
	"testing"

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

	"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
)

// 成员/角色 ACL（autonomy roadmap §11）测试。证明：member(editor) 可写、viewer
// 只读、非成员对 private 仍被拒、owner/admin 行为不变、project-admin 可管理成员、
// 跨项目成员隔离。

func TestMembers_ViewerCanReadNotWrite(t *testing.T) {
	repo := newFakeRepo()
	owner := uuid.New()
	viewer := uuid.New()
	p := seedProject(t, repo, owner) // private
	_, err := repo.UpsertMember(context.Background(), p.ID, viewer, RoleViewer, &owner)
	require.NoError(t, err)

	svc := NewProjectService(repo, nil)
	c := Caller{UserID: viewer}

	// 读：viewer 可读 private 项目（成员叠加读权限）。
	got, err := svc.Get(context.Background(), c, "demo")
	require.NoError(t, err)
	require.Equal(t, "demo", got.Slug)

	// 写：viewer 不可写。能读但不能写 → forbidden（其存在性已可见）。
	name := "Renamed"
	_, err = svc.Update(context.Background(), c, "demo", UpdateProjectInput{Name: &name})
	ae, ok := errs.As(err)
	require.True(t, ok)
	require.Equal(t, errs.CodeForbidden, ae.Code)
}

func TestMembers_EditorCanWriteRequirements(t *testing.T) {
	repo := newFakeRepo()
	owner := uuid.New()
	editor := uuid.New()
	p := seedProject(t, repo, owner) // private
	_, err := repo.UpsertMember(context.Background(), p.ID, editor, RoleMember, &owner)
	require.NoError(t, err)

	reqSvc := NewRequirementService(repo, nil, nil, nil)
	c := Caller{UserID: editor}

	// member 角色叠加写权限：可在 private 项目创建需求。
	got, err := reqSvc.Create(context.Background(), c, "demo", CreateRequirementInput{Title: "feat"})
	require.NoError(t, err)
	require.Equal(t, "feat", got.Title)
	require.Equal(t, editor, got.OwnerID, "默认 owner 为创建者")

	// member 也可读。
	_, err = reqSvc.Get(context.Background(), c, "demo", got.Number)
	require.NoError(t, err)
}

func TestMembers_ViewerCannotWriteRequirements(t *testing.T) {
	repo := newFakeRepo()
	owner := uuid.New()
	viewer := uuid.New()
	p := seedProject(t, repo, owner)
	_, err := repo.UpsertMember(context.Background(), p.ID, viewer, RoleViewer, &owner)
	require.NoError(t, err)

	reqSvc := NewRequirementService(repo, nil, nil, nil)
	_, err = reqSvc.Create(context.Background(), Caller{UserID: viewer}, "demo", CreateRequirementInput{Title: "x"})
	ae, ok := errs.As(err)
	require.True(t, ok)
	require.Equal(t, errs.CodeForbidden, ae.Code, "viewer 能读但不能写 → forbidden")

	// viewer 仍可读需求列表。
	_, err = reqSvc.List(context.Background(), Caller{UserID: viewer}, "demo", RequirementFilter{})
	require.NoError(t, err)
}

func TestMembers_NonMemberDeniedOnPrivate(t *testing.T) {
	repo := newFakeRepo()
	owner := uuid.New()
	seedProject(t, repo, owner) // private, 无其它成员
	svc := NewProjectService(repo, nil)

	stranger := Caller{UserID: uuid.New()}
	// 非成员看 private：屏蔽存在性 → not_found（既有防探测语义保持）。
	_, err := svc.Get(context.Background(), stranger, "demo")
	ae, ok := errs.As(err)
	require.True(t, ok)
	require.Equal(t, errs.CodeNotFound, ae.Code)

	// 非成员写：同样 not_found（既有语义）。
	name := "X"
	_, err = svc.Update(context.Background(), stranger, "demo", UpdateProjectInput{Name: &name})
	ae, ok = errs.As(err)
	require.True(t, ok)
	require.Equal(t, errs.CodeNotFound, ae.Code)
}

func TestMembers_OwnerAndAdminUnchanged(t *testing.T) {
	repo := newFakeRepo()
	owner := uuid.New()
	seedProject(t, repo, owner) // private, 无 project_members 行（模拟未回填或被移除）
	svc := NewProjectService(repo, nil)

	// owner 即便没有 project_members 行也可读写（OwnerID 兜底）。
	name := "ByOwner"
	_, err := svc.Update(context.Background(), Caller{UserID: owner}, "demo", UpdateProjectInput{Name: &name})
	require.NoError(t, err)

	// global-admin 旁路：可读写任意 private 项目。
	name2 := "ByAdmin"
	_, err = svc.Update(context.Background(), Caller{UserID: uuid.New(), IsAdmin: true}, "demo", UpdateProjectInput{Name: &name2})
	require.NoError(t, err)
	got, err := svc.Get(context.Background(), Caller{UserID: uuid.New(), IsAdmin: true}, "demo")
	require.NoError(t, err)
	require.Equal(t, "ByAdmin", got.Name)
}

func TestMembers_ProjectAdminCanManageMembers(t *testing.T) {
	repo := newFakeRepo()
	owner := uuid.New()
	padmin := uuid.New()
	target := uuid.New()
	p := seedProject(t, repo, owner)
	_, err := repo.UpsertMember(context.Background(), p.ID, padmin, RoleAdmin, &owner)
	require.NoError(t, err)

	svc := NewProjectService(repo, nil)
	cAdmin := Caller{UserID: padmin}

	// project-admin 可新增成员。
	m, err := svc.AddMember(context.Background(), cAdmin, "demo", AddMemberInput{UserID: target, Role: RoleMember})
	require.NoError(t, err)
	require.Equal(t, RoleMember, m.Role)

	// project-admin 可改角色（upsert）。
	m, err = svc.AddMember(context.Background(), cAdmin, "demo", AddMemberInput{UserID: target, Role: RoleViewer})
	require.NoError(t, err)
	require.Equal(t, RoleViewer, m.Role)

	// project-admin 可列成员。
	members, err := svc.ListMembers(context.Background(), cAdmin, "demo")
	require.NoError(t, err)
	require.NotEmpty(t, members)

	// project-admin 可移除成员。
	require.NoError(t, svc.RemoveMember(context.Background(), cAdmin, "demo", target))
	role, err := repo.GetMemberRole(context.Background(), p.ID, target)
	require.NoError(t, err)
	require.Equal(t, RoleNone, role)

	// 不能移除 owner。
	err = svc.RemoveMember(context.Background(), cAdmin, "demo", owner)
	ae, ok := errs.As(err)
	require.True(t, ok)
	require.Equal(t, errs.CodeForbidden, ae.Code)
}

func TestMembers_NonAdminCannotManageMembers(t *testing.T) {
	repo := newFakeRepo()
	owner := uuid.New()
	editor := uuid.New()
	p := seedProject(t, repo, owner)
	_, err := repo.UpsertMember(context.Background(), p.ID, editor, RoleMember, &owner)
	require.NoError(t, err)

	svc := NewProjectService(repo, nil)
	// member(editor) 不可管理成员 → forbidden（能读，但无管理权）。
	_, err = svc.AddMember(context.Background(), Caller{UserID: editor}, "demo", AddMemberInput{UserID: uuid.New(), Role: RoleViewer})
	ae, ok := errs.As(err)
	require.True(t, ok)
	require.Equal(t, errs.CodeForbidden, ae.Code)

	// 非成员对 private 管理成员 → not_found 防探测。
	_, err = svc.AddMember(context.Background(), Caller{UserID: uuid.New()}, "demo", AddMemberInput{UserID: uuid.New(), Role: RoleViewer})
	ae, ok = errs.As(err)
	require.True(t, ok)
	require.Equal(t, errs.CodeNotFound, ae.Code)
}

func TestMembers_AddMemberValidatesInput(t *testing.T) {
	repo := newFakeRepo()
	owner := uuid.New()
	seedProject(t, repo, owner)
	svc := NewProjectService(repo, nil)

	// 非法角色被拒。
	_, err := svc.AddMember(context.Background(), Caller{UserID: owner}, "demo", AddMemberInput{UserID: uuid.New(), Role: Role("superuser")})
	ae, ok := errs.As(err)
	require.True(t, ok)
	require.Equal(t, errs.CodeInvalidInput, ae.Code)

	// 空 user_id 被拒。
	_, err = svc.AddMember(context.Background(), Caller{UserID: owner}, "demo", AddMemberInput{UserID: uuid.Nil, Role: RoleViewer})
	ae, ok = errs.As(err)
	require.True(t, ok)
	require.Equal(t, errs.CodeInvalidInput, ae.Code)
}

func TestMembers_CrossProjectIsolation(t *testing.T) {
	repo := newFakeRepo()
	ownerA := uuid.New()
	ownerB := uuid.New()
	pa := seedProject(t, repo, ownerA) // slug "demo"

	pb := &Project{ID: uuid.New(), Slug: "other", Name: "Other", Visibility: VisibilityPrivate, OwnerID: ownerB}
	_, err := repo.CreateProject(context.Background(), pb)
	require.NoError(t, err)

	// 用户是 A 项目的 member，但不是 B 项目成员。
	member := uuid.New()
	_, err = repo.UpsertMember(context.Background(), pa.ID, member, RoleMember, &ownerA)
	require.NoError(t, err)

	svc := NewProjectService(repo, nil)

	// A 项目可写。
	nameA := "A-renamed"
	_, err = svc.Update(context.Background(), Caller{UserID: member}, "demo", UpdateProjectInput{Name: &nameA})
	require.NoError(t, err)

	// B 项目（private，非成员）→ not_found。
	nameB := "B-renamed"
	_, err = svc.Update(context.Background(), Caller{UserID: member}, "other", UpdateProjectInput{Name: &nameB})
	ae, ok := errs.As(err)
	require.True(t, ok)
	require.Equal(t, errs.CodeNotFound, ae.Code)
}

func TestMembers_ListIncludesMembershipProjects(t *testing.T) {
	repo := newFakeRepo()
	owner := uuid.New()
	pa := seedProject(t, repo, owner) // private "demo"

	viewer := uuid.New()
	_, err := repo.UpsertMember(context.Background(), pa.ID, viewer, RoleViewer, &owner)
	require.NoError(t, err)

	svc := NewProjectService(repo, nil)
	// viewer 列项目应包含其作为成员的 private 项目。
	got, err := svc.List(context.Background(), Caller{UserID: viewer}, false)
	require.NoError(t, err)
	require.Len(t, got, 1)
	require.Equal(t, "demo", got[0].Slug)

	// 完全无关用户列不到该 private 项目。
	got, err = svc.List(context.Background(), Caller{UserID: uuid.New()}, false)
	require.NoError(t, err)
	require.Empty(t, got)
}
