package users_service

import (
	"context"
	"errors"
	"evening_detective_server/internal/modules/email_sender"
	"evening_detective_server/internal/modules/password_generator"
	"evening_detective_server/internal/modules/processor_jwt"
	"evening_detective_server/internal/repos"
	"evening_detective_server/internal/repos/refresh_tokens_repo"
	"evening_detective_server/internal/repos/user_agreements_repo"
	"evening_detective_server/internal/repos/users_repo"
	"strings"
	"sync"
	"testing"

	"github.com/jackc/pgx/v5/pgxpool"
)

type fakeSender struct {
	mu       sync.Mutex
	messages []email_sender.Message
	err      error
}

func (f *fakeSender) Send(_ context.Context, m email_sender.Message) error {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.messages = append(f.messages, m)
	return f.err
}

func (f *fakeSender) sent() []email_sender.Message {
	f.mu.Lock()
	defer f.mu.Unlock()
	return append([]email_sender.Message(nil), f.messages...)
}

// fakeRepo — минимальная in-memory реализация usersRepository для тестов
// сервиса без БД.
type fakeRepo struct {
	mu     sync.Mutex
	users  map[string]*repos.User
	byID   map[int]*repos.User
	nextID int
}

func newFakeRepo() *fakeRepo {
	return &fakeRepo{
		users:  map[string]*repos.User{},
		byID:   map[int]*repos.User{},
		nextID: 1,
	}
}

func (r *fakeRepo) AddUserWithAgreements(
	_ context.Context,
	username, email, passwordHash string,
	roles []string,
	_ []user_agreements_repo.Agreement,
) (int, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	if _, ok := r.users[email]; ok {
		return 0, errors.New("Username или email уже используется")
	}
	id := r.nextID
	r.nextID++
	u := &repos.User{ID: id, Username: username, Email: email, PasswordHash: passwordHash, Roles: roles}
	r.users[email] = u
	r.byID[id] = u
	return id, nil
}

func (r *fakeRepo) UpdateUserPassword(_ context.Context, email, passwordHash string) error {
	r.mu.Lock()
	defer r.mu.Unlock()
	u, ok := r.users[email]
	if !ok {
		return users_repo.ErrUserNotFound
	}
	u.PasswordHash = passwordHash
	return nil
}

func (r *fakeRepo) GetUserByEmail(_ context.Context, email string) (*repos.User, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	u, ok := r.users[email]
	if !ok {
		return nil, users_repo.ErrUserNotFound
	}
	return u, nil
}

func (r *fakeRepo) GetUserByID(_ context.Context, id int) (*repos.User, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	u, ok := r.byID[id]
	if !ok {
		return nil, users_repo.ErrUserNotFound
	}
	return u, nil
}

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

func (r *fakeRepo) AddUserRole(_ context.Context, _ int, _ string) error { return nil }

func (r *fakeRepo) DeleteUserRole(_ context.Context, _ int, _ string) error { return nil }

func (r *fakeRepo) DeleteUser(_ context.Context, userId int) error {
	r.mu.Lock()
	defer r.mu.Unlock()
	u, ok := r.byID[userId]
	if !ok {
		return users_repo.ErrUserNotFound
	}
	delete(r.byID, userId)
	delete(r.users, u.Email)
	return nil
}

func (r *fakeRepo) userCount() int {
	r.mu.Lock()
	defer r.mu.Unlock()
	return len(r.users)
}

func (r *fakeRepo) seed(email, passwordHash string) {
	r.mu.Lock()
	defer r.mu.Unlock()
	u := &repos.User{ID: r.nextID, Email: email, PasswordHash: passwordHash}
	r.nextID++
	r.users[email] = u
	r.byID[u.ID] = u
}

func newTestService(repo usersRepository, sender email_sender.IEmailSender) *UsersService {
	return newTestServiceWithBaseURL(repo, sender, "")
}

func newTestServiceWithBaseURL(repo usersRepository, sender email_sender.IEmailSender, baseURL string) *UsersService {
	// Пул создаётся лениво (pgxpool.New не контактирует с БД) и в этих
	// тестах не используется.
	pool, err := pgxpool.New(context.Background(), "postgres://postgres:postgres@localhost:5432/none")
	if err != nil {
		panic(err)
	}
	return NewUsersService(
		repo,
		password_generator.NewGenerator(12),
		sender,
		processor_jwt.NewProcessor("test-secret"),
		refresh_tokens_repo.NewRefreshTokensRepo(pool),
		baseURL,
	)
}

func TestAddUserSuccess(t *testing.T) {
	repo := newFakeRepo()
	sender := &fakeSender{}
	svc := newTestService(repo, sender)

	err := svc.AddUser(context.Background(), "detective", " User@Example.COM ", true, true, "1.2.3.4", "ua")
	if err != nil {
		t.Fatalf("AddUser: %v", err)
	}

	// Email нормализован: пробелы убраны, регистр приведён.
	if _, ok := repo.users["user@example.com"]; !ok {
		t.Errorf("пользователь не создан с нормализованным email: %+v", repo.users)
	}

	msgs := sender.sent()
	if len(msgs) != 1 {
		t.Fatalf("ожидалось 1 письмо, отправлено %d", len(msgs))
	}
	if msgs[0].To != "user@example.com" {
		t.Errorf("письмо ушло на %q, want user@example.com", msgs[0].To)
	}
	if !strings.Contains(msgs[0].Subject, "Вечернему детективу") {
		t.Errorf("неожиданная тема письма: %q", msgs[0].Subject)
	}
	if !strings.Contains(msgs[0].Body, "пароль") {
		t.Error("в текстовой версии письма не упоминается пароль")
	}
	if msgs[0].HTML == "" {
		t.Error("у письма нет HTML-версии — Gmail хуже рендерит такие письма")
	}
	// APP_BASE_URL в тесте пуст — кнопка «Войти в игру» не добавляется,
	// но каркас письма (шапка сервиса) обязан присутствовать.
	if !strings.Contains(msgs[0].HTML, "ВЕЧЕРНИЙ ДЕТЕКТИВ") {
		t.Error("HTML-версия письма не содержит шапку сервиса")
	}
	if strings.Contains(msgs[0].HTML, "Войти в игру") {
		t.Error("кнопка входа не должна добавляться без APP_BASE_URL")
	}
}

func TestAddUserWithBaseURLAddsLoginLink(t *testing.T) {
	repo := newFakeRepo()
	sender := &fakeSender{}
	svc := newTestServiceWithBaseURL(repo, sender, "https://evening-detective.crabs-games.art/")

	err := svc.AddUser(context.Background(), "detective", "user@example.com", true, true, "", "")
	if err != nil {
		t.Fatalf("AddUser: %v", err)
	}

	msgs := sender.sent()
	if len(msgs) != 1 {
		t.Fatalf("ожидалось 1 письмо, отправлено %d", len(msgs))
	}
	// Завершающий слэш срезается, ссылка есть и в plain, и в HTML-версии.
	if !strings.Contains(msgs[0].Body, "https://evening-detective.crabs-games.art") {
		t.Errorf("plain-версия не содержит ссылку на вход:\n%s", msgs[0].Body)
	}
	if !strings.Contains(msgs[0].HTML, "https://evening-detective.crabs-games.art") {
		t.Error("HTML-версия не содержит ссылку на вход")
	}
	if !strings.Contains(msgs[0].HTML, "Войти в игру") {
		t.Error("HTML-версия не содержит кнопку «Войти в игру»")
	}
}

func TestLoginURLInvalid(t *testing.T) {
	for _, in := range []string{"", "   ", "ftp://example.com", "not-a-url", "javascript:alert(1)"} {
		if got := loginURL(in); got != "" {
			t.Errorf("loginURL(%q) = %q, want пусто", in, got)
		}
	}
}

func TestAddUserSendFailureCompensates(t *testing.T) {
	repo := newFakeRepo()
	sender := &fakeSender{err: errors.New("smtp down")}
	svc := newTestService(repo, sender)

	err := svc.AddUser(context.Background(), "detective", "user@example.com", true, true, "", "")
	if err == nil {
		t.Fatal("ожидалась ошибка при сбое отправки письма")
	}

	// Компенсация: клиенту ошибка, пользователя в БД нет.
	if repo.userCount() != 0 {
		t.Errorf("пользователь остался после ошибки отправки: %+v", repo.users)
	}
}

func TestAddUserInvalidEmail(t *testing.T) {
	repo := newFakeRepo()
	svc := newTestService(repo, &fakeSender{})

	err := svc.AddUser(context.Background(), "detective", "not-an-email", true, true, "", "")
	if err == nil {
		t.Fatal("ожидалась ошибка валидации email")
	}
	if repo.userCount() != 0 {
		t.Error("пользователь не должен создаваться при невалидном email")
	}
}

func TestAddUserRequiresTerms(t *testing.T) {
	repo := newFakeRepo()
	svc := newTestService(repo, &fakeSender{})

	err := svc.AddUser(context.Background(), "detective", "user@example.com", false, true, "", "")
	if !errors.Is(err, ErrTermsNotAccepted) {
		t.Fatalf("ожидался ErrTermsNotAccepted, получено %v", err)
	}
	if repo.userCount() != 0 {
		t.Error("пользователь не должен создаваться без акцепта")
	}
}

func TestRefreshPasswordSuccess(t *testing.T) {
	repo := newFakeRepo()
	repo.seed("user@example.com", "old-hash")
	sender := &fakeSender{}
	svc := newTestService(repo, sender)

	// Ввод с пробелами и другим регистром — нормализуется.
	err := svc.RefreshPassword(context.Background(), "  User@Example.COM ")
	if err != nil {
		t.Fatalf("RefreshPassword: %v", err)
	}

	if len(sender.sent()) != 1 {
		t.Fatalf("ожидалось 1 письмо, отправлено %d", len(sender.sent()))
	}
	u, _ := repo.GetUserByEmail(context.Background(), "user@example.com")
	if u.PasswordHash == "old-hash" {
		t.Error("пароль не изменён")
	}
}

func TestRefreshPasswordSendFailureKeepsPassword(t *testing.T) {
	repo := newFakeRepo()
	repo.seed("user@example.com", "old-hash")
	sender := &fakeSender{err: errors.New("smtp down")}
	svc := newTestService(repo, sender)

	err := svc.RefreshPassword(context.Background(), "user@example.com")
	if err == nil {
		t.Fatal("ожидалась ошибка при сбое отправки")
	}

	u, _ := repo.GetUserByEmail(context.Background(), "user@example.com")
	if u.PasswordHash != "old-hash" {
		t.Error("пароль изменён, хотя письмо не ушло — аккаунт заблокирован")
	}
}

func TestRefreshPasswordUnknownEmail(t *testing.T) {
	repo := newFakeRepo()
	sender := &fakeSender{}
	svc := newTestService(repo, sender)

	// Анти-enumeration: для несуществующего адреса — успех и без письма.
	if err := svc.RefreshPassword(context.Background(), "nobody@example.com"); err != nil {
		t.Fatalf("для несуществующего email ожидался успех, получено %v", err)
	}
	if len(sender.sent()) != 0 {
		t.Error("письмо не должно уходить несуществующему пользователю")
	}
}

func TestRefreshPasswordInvalidEmail(t *testing.T) {
	repo := newFakeRepo()
	sender := &fakeSender{}
	svc := newTestService(repo, sender)

	if err := svc.RefreshPassword(context.Background(), "not-an-email"); err == nil {
		t.Fatal("ожидалась ошибка валидации email")
	}
	if len(sender.sent()) != 0 {
		t.Error("письмо не должно уходить при невалидном email")
	}
}
