package user

import (
	"context"
	"testing"
	"time"

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

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

// fakeRepo is an in-memory Repository implementation used by the service
// unit tests. It deliberately stores users keyed by email and sessions keyed
// by the string form of TokenHash so lookups stay O(1) and the test setup
// doesn't need to reach into pgx-specific machinery.
type fakeRepo struct {
	users    map[string]*User
	sessions map[string]*Session
}

func newFakeRepo() *fakeRepo {
	return &fakeRepo{users: map[string]*User{}, sessions: map[string]*Session{}}
}

func (r *fakeRepo) CreateUser(_ context.Context, u *User) (*User, error) {
	r.users[u.Email] = u
	return u, nil
}

func (r *fakeRepo) GetUserByEmail(_ context.Context, email string) (*User, error) {
	u, ok := r.users[email]
	if !ok {
		return nil, errs.New(errs.CodeNotFound, "no user")
	}
	return u, nil
}

func (r *fakeRepo) GetUserByID(_ context.Context, id uuid.UUID) (*User, error) {
	for _, u := range r.users {
		if u.ID == id {
			return u, nil
		}
	}
	return nil, errs.New(errs.CodeNotFound, "no user")
}

func (r *fakeRepo) CountUsers(_ context.Context) (int, error) { return len(r.users), nil }
func (r *fakeRepo) ListAdmins(_ context.Context) ([]*User, error) {
	out := make([]*User, 0)
	for _, u := range r.users {
		if u.IsAdmin {
			out = append(out, u)
		}
	}
	return out, nil
}

func (r *fakeRepo) CountAdmins(_ context.Context) (int, error) {
	n := 0
	for _, u := range r.users {
		if u.IsAdmin && u.Enabled {
			n++
		}
	}
	return n, nil
}

func (r *fakeRepo) ListUsers(_ context.Context) ([]*User, error) {
	out := make([]*User, 0, len(r.users))
	for _, u := range r.users {
		out = append(out, u)
	}
	return out, nil
}

func (r *fakeRepo) findByID(id uuid.UUID) (*User, bool) {
	for _, u := range r.users {
		if u.ID == id {
			return u, true
		}
	}
	return nil, false
}

func (r *fakeRepo) UpdateProfile(_ context.Context, id uuid.UUID, displayName string) (*User, error) {
	u, ok := r.findByID(id)
	if !ok {
		return nil, errs.New(errs.CodeNotFound, "no user")
	}
	u.DisplayName = displayName
	return u, nil
}

func (r *fakeRepo) SetAdmin(_ context.Context, id uuid.UUID, isAdmin bool) (*User, error) {
	u, ok := r.findByID(id)
	if !ok {
		return nil, errs.New(errs.CodeNotFound, "no user")
	}
	u.IsAdmin = isAdmin
	return u, nil
}

func (r *fakeRepo) SetEnabled(_ context.Context, id uuid.UUID, enabled bool) (*User, error) {
	u, ok := r.findByID(id)
	if !ok {
		return nil, errs.New(errs.CodeNotFound, "no user")
	}
	u.Enabled = enabled
	return u, nil
}

func (r *fakeRepo) UpdatePassword(_ context.Context, id uuid.UUID, passwordHash string) error {
	u, ok := r.findByID(id)
	if !ok {
		return errs.New(errs.CodeNotFound, "no user")
	}
	u.PasswordHash = passwordHash
	return nil
}

func (r *fakeRepo) DeleteUser(_ context.Context, id uuid.UUID) error {
	for email, u := range r.users {
		if u.ID == id {
			delete(r.users, email)
			return nil
		}
	}
	return nil
}

func (r *fakeRepo) DeleteSessionsByUserID(_ context.Context, userID uuid.UUID) error {
	for k, s := range r.sessions {
		if s.UserID == userID {
			delete(r.sessions, k)
		}
	}
	return nil
}

func (r *fakeRepo) CreateSession(_ context.Context, s *Session) error {
	r.sessions[string(s.TokenHash)] = s
	return nil
}

func (r *fakeRepo) GetSessionByTokenHash(_ context.Context, h []byte) (*Session, error) {
	s, ok := r.sessions[string(h)]
	if !ok || s.ExpiresAt.Before(time.Now()) {
		return nil, errs.New(errs.CodeUnauthorized, "no session")
	}
	return s, nil
}

func (r *fakeRepo) TouchSession(_ context.Context, _ []byte) error { return nil }

func (r *fakeRepo) DeleteSessionByTokenHash(_ context.Context, h []byte) error {
	delete(r.sessions, string(h))
	return nil
}

// newServiceWithUser wires a *service backed by a fakeRepo that already
// contains a single user with the given credentials. The returned *service
// is the concrete type (not the Service interface) so individual tests can
// poke at internal hooks like `now` if a future case needs it.
func newServiceWithUser(t *testing.T, email, password string) (*service, *User) {
	t.Helper()
	repo := newFakeRepo()
	hash, err := HashPassword(password)
	require.NoError(t, err)
	u := &User{ID: uuid.New(), Email: email, DisplayName: "Test", PasswordHash: hash}
	repo.users[email] = u
	svc := NewService(repo, nil).(*service)
	return svc, u
}

func TestLogin_Success(t *testing.T) {
	svc, u := newServiceWithUser(t, "a@b.c", "password123")
	tok, gotUser, err := svc.Login(context.Background(), "a@b.c", "password123", "127.0.0.1")
	require.NoError(t, err)
	require.NotEmpty(t, tok)
	require.Equal(t, u.ID, gotUser.ID)
}

func TestLogin_BadPassword(t *testing.T) {
	svc, _ := newServiceWithUser(t, "a@b.c", "password123")
	_, _, err := svc.Login(context.Background(), "a@b.c", "wrong-pass", "ip")
	require.Error(t, err)
	ae, ok := errs.As(err)
	require.True(t, ok)
	require.Equal(t, errs.CodeUnauthorized, ae.Code)
}

func TestLogin_UnknownEmail(t *testing.T) {
	svc, _ := newServiceWithUser(t, "a@b.c", "password123")
	_, _, err := svc.Login(context.Background(), "x@b.c", "x", "ip")
	require.Error(t, err)
	ae, ok := errs.As(err)
	require.True(t, ok)
	require.Equal(t, errs.CodeUnauthorized, ae.Code, "should not leak whether email exists")
}

func TestResolveSession_Success(t *testing.T) {
	svc, u := newServiceWithUser(t, "a@b.c", "password123")
	tok, _, err := svc.Login(context.Background(), "a@b.c", "password123", "ip")
	require.NoError(t, err)

	uid, ok, err := svc.ResolveSession(context.Background(), tok)
	require.NoError(t, err)
	require.True(t, ok)
	require.Equal(t, u.ID, uid)
}

func TestLogout_RemovesSession(t *testing.T) {
	svc, _ := newServiceWithUser(t, "a@b.c", "password123")
	tok, _, err := svc.Login(context.Background(), "a@b.c", "password123", "ip")
	require.NoError(t, err)
	require.NoError(t, svc.Logout(context.Background(), tok))

	_, ok, err := svc.ResolveSession(context.Background(), tok)
	require.NoError(t, err)
	require.False(t, ok)
}

type spyAudit struct{ entries []audit.Entry }

func (s *spyAudit) Record(_ context.Context, e audit.Entry) error {
	s.entries = append(s.entries, e)
	return nil
}

func TestLogin_RecordsAudit(t *testing.T) {
	repo := newFakeRepo()
	hash, err := HashPassword("password123")
	require.NoError(t, err)
	repo.users["a@b.c"] = &User{ID: uuid.New(), Email: "a@b.c", DisplayName: "T", PasswordHash: hash}
	spy := &spyAudit{}
	svc := NewService(repo, spy)

	_, _, err = svc.Login(context.Background(), "a@b.c", "password123", "127.0.0.1")
	require.NoError(t, err)
	require.Len(t, spy.entries, 1)
	require.Equal(t, "user.login", spy.entries[0].Action)
	require.Equal(t, "127.0.0.1", spy.entries[0].IP)
	require.NotNil(t, spy.entries[0].UserID)
}

func TestLogout_RecordsAudit(t *testing.T) {
	svc, _ := newServiceWithUser(t, "a@b.c", "password123")
	// 先用 nil audit 走一遍 Login
	tok, _, err := svc.Login(context.Background(), "a@b.c", "password123", "127.0.0.1")
	require.NoError(t, err)
	// 替换 audit recorder 注入 spy 后 Logout
	spy := &spyAudit{}
	svc.audit = spy
	require.NoError(t, svc.Logout(context.Background(), tok))
	require.Len(t, spy.entries, 1)
	require.Equal(t, "user.logout", spy.entries[0].Action)
}

// ===== 管理员用户管理测试 =====

// newAdminRepo 构造一个含一个启用管理员的 fakeRepo + service。
func newAdminRepo(t *testing.T) (*service, *fakeRepo, *User) {
	t.Helper()
	repo := newFakeRepo()
	hash, err := HashPassword("password123")
	require.NoError(t, err)
	admin := &User{ID: uuid.New(), Email: "admin@b.c", DisplayName: "Admin", PasswordHash: hash, IsAdmin: true, Enabled: true}
	repo.users[admin.Email] = admin
	svc := NewService(repo, nil).(*service)
	return svc, repo, admin
}

func TestCreateUser_AndListUsers(t *testing.T) {
	svc, _, _ := newAdminRepo(t)
	u, err := svc.CreateUser(context.Background(), CreateUserInput{
		Email: "new@b.c", DisplayName: "New", Password: "password123", IsAdmin: false,
	})
	require.NoError(t, err)
	require.Equal(t, "new@b.c", u.Email)
	users, err := svc.ListUsers(context.Background())
	require.NoError(t, err)
	require.Len(t, users, 2)
}

func TestCreateUser_ShortPassword(t *testing.T) {
	svc, _, _ := newAdminRepo(t)
	_, err := svc.CreateUser(context.Background(), CreateUserInput{
		Email: "new@b.c", DisplayName: "New", Password: "short",
	})
	ae, ok := errs.As(err)
	require.True(t, ok)
	require.Equal(t, errs.CodeInvalidInput, ae.Code)
}

func TestSetAdmin_CannotDemoteSelf(t *testing.T) {
	svc, _, admin := newAdminRepo(t)
	_, err := svc.SetAdmin(context.Background(), admin.ID, admin.ID, false)
	ae, ok := errs.As(err)
	require.True(t, ok)
	require.Equal(t, errs.CodeConflict, ae.Code)
}

func TestSetAdmin_CannotDemoteLastAdmin(t *testing.T) {
	svc, repo, admin := newAdminRepo(t)
	// 加一个普通用户作 actor，避免触发自锁分支
	actor := &User{ID: uuid.New(), Email: "actor@b.c", DisplayName: "A", IsAdmin: true, Enabled: true}
	repo.users[actor.Email] = actor
	// 现在有 2 个 admin；降级其中一个应成功
	_, err := svc.SetAdmin(context.Background(), actor.ID, admin.ID, false)
	require.NoError(t, err)
	// 只剩 actor 一个 admin；再降级 actor（由自己以外触发不可能，构造另一普通用户）
	plain := &User{ID: uuid.New(), Email: "p@b.c", DisplayName: "P", IsAdmin: false, Enabled: true}
	repo.users[plain.Email] = plain
	_, err = svc.SetAdmin(context.Background(), plain.ID, actor.ID, false)
	ae, ok := errs.As(err)
	require.True(t, ok)
	require.Equal(t, errs.CodeConflict, ae.Code)
}

func TestSetEnabled_DisableRevokesSessions(t *testing.T) {
	svc, repo, _ := newAdminRepo(t)
	target := &User{ID: uuid.New(), Email: "t@b.c", DisplayName: "T", IsAdmin: false, Enabled: true}
	repo.users[target.Email] = target
	repo.sessions["s1"] = &Session{UserID: target.ID, TokenHash: []byte("s1")}
	actor := &User{ID: uuid.New(), Email: "act@b.c", IsAdmin: true, Enabled: true}
	repo.users[actor.Email] = actor
	_, err := svc.SetEnabled(context.Background(), actor.ID, target.ID, false)
	require.NoError(t, err)
	require.False(t, repo.users["t@b.c"].Enabled)
	require.Empty(t, repo.sessions)
}

func TestChangePassword_WrongOld(t *testing.T) {
	svc, _, admin := newAdminRepo(t)
	err := svc.ChangePassword(context.Background(), admin.ID, "wrong-old", "newpassword123")
	ae, ok := errs.As(err)
	require.True(t, ok)
	require.Equal(t, errs.CodeUnauthorized, ae.Code)
}

func TestChangePassword_Success(t *testing.T) {
	svc, repo, admin := newAdminRepo(t)
	repo.sessions["s1"] = &Session{UserID: admin.ID, TokenHash: []byte("s1")}
	err := svc.ChangePassword(context.Background(), admin.ID, "password123", "newpassword123")
	require.NoError(t, err)
	ok, err := VerifyPassword("newpassword123", repo.users["admin@b.c"].PasswordHash)
	require.NoError(t, err)
	require.True(t, ok)
	require.Empty(t, repo.sessions) // 改密撤销全部会话
}

func TestAdminResetPassword_ReturnsTemp(t *testing.T) {
	svc, repo, admin := newAdminRepo(t)
	temp, err := svc.AdminResetPassword(context.Background(), admin.ID)
	require.NoError(t, err)
	require.GreaterOrEqual(t, len(temp), minPasswordLen)
	ok, err := VerifyPassword(temp, repo.users["admin@b.c"].PasswordHash)
	require.NoError(t, err)
	require.True(t, ok)
}

func TestDeleteUser_CannotDeleteSelf(t *testing.T) {
	svc, _, admin := newAdminRepo(t)
	err := svc.DeleteUser(context.Background(), admin.ID, admin.ID)
	ae, ok := errs.As(err)
	require.True(t, ok)
	require.Equal(t, errs.CodeConflict, ae.Code)
}

func TestDeleteUser_CannotDeleteLastAdmin(t *testing.T) {
	svc, repo, admin := newAdminRepo(t)
	actor := &User{ID: uuid.New(), Email: "act@b.c", IsAdmin: false, Enabled: true}
	repo.users[actor.Email] = actor
	err := svc.DeleteUser(context.Background(), actor.ID, admin.ID)
	ae, ok := errs.As(err)
	require.True(t, ok)
	require.Equal(t, errs.CodeConflict, ae.Code)
}
