import { describe, it, expect, vi, beforeEach } from 'vitest';
import { UserService } from '../src/services/user.service.js';
import { NotFoundError, ConflictError } from '../src/services/mcp-server.service.js';
import type { IUserRepository, SafeUser } from '../src/repositories/user.repository.js';

function makeSafeUser(overrides: Partial<SafeUser> = {}): SafeUser {
  return {
    id: 'user-1',
    email: 'alice@example.com',
    name: 'Alice',
    role: 'USER',
    provider: null,
    externalId: null,
    version: 1,
    createdAt: new Date(),
    updatedAt: new Date(),
    ...overrides,
  };
}

function mockUserRepo(): IUserRepository {
  return {
    findAll: vi.fn(async () => []),
    findById: vi.fn(async () => null),
    findByEmail: vi.fn(async () => null),
    create: vi.fn(async (data) =>
      makeSafeUser({ email: data.email, name: data.name ?? null }),
    ),
    delete: vi.fn(async () => {}),
    count: vi.fn(async () => 0),
  };
}

describe('UserService', () => {
  let repo: ReturnType<typeof mockUserRepo>;
  let service: UserService;

  beforeEach(() => {
    repo = mockUserRepo();
    service = new UserService(repo);
  });

  // ── list ──────────────────────────────────────────────────

  describe('list', () => {
    it('returns empty array when no users', async () => {
      const result = await service.list();
      expect(result).toEqual([]);
      expect(repo.findAll).toHaveBeenCalledOnce();
    });

    it('returns all users', async () => {
      const users = [
        makeSafeUser({ id: 'u1', email: 'a@b.com' }),
        makeSafeUser({ id: 'u2', email: 'c@d.com' }),
      ];
      vi.mocked(repo.findAll).mockResolvedValue(users);

      const result = await service.list();
      expect(result).toHaveLength(2);
      expect(result[0]!.email).toBe('a@b.com');
    });
  });

  // ── create ────────────────────────────────────────────────

  describe('create', () => {
    it('creates a user and hashes password', async () => {
      const result = await service.create({
        email: 'alice@example.com',
        password: 'securePass123',
      });

      expect(result.email).toBe('alice@example.com');
      expect(repo.create).toHaveBeenCalledOnce();

      // Verify the passwordHash was generated (not the plain password)
      const createCall = vi.mocked(repo.create).mock.calls[0]![0]!;
      expect(createCall.passwordHash).toBeDefined();
      expect(createCall.passwordHash).not.toBe('securePass123');
      expect(createCall.passwordHash.startsWith('$2b$')).toBe(true);
    });

    it('creates a user with optional name', async () => {
      await service.create({
        email: 'bob@example.com',
        password: 'securePass123',
        name: 'Bob',
      });

      const createCall = vi.mocked(repo.create).mock.calls[0]![0]!;
      expect(createCall.email).toBe('bob@example.com');
      expect(createCall.name).toBe('Bob');
    });

    it('returns user without passwordHash', async () => {
      const result = await service.create({
        email: 'alice@example.com',
        password: 'securePass123',
      });

      // SafeUser type should not have passwordHash
      expect(result).not.toHaveProperty('passwordHash');
    });

    it('throws ConflictError when email already exists', async () => {
      vi.mocked(repo.findByEmail).mockResolvedValue(makeSafeUser());

      await expect(
        service.create({ email: 'alice@example.com', password: 'securePass123' }),
      ).rejects.toThrow(ConflictError);
    });

    it('throws ZodError for invalid email', async () => {
      await expect(
        service.create({ email: 'not-an-email', password: 'securePass123' }),
      ).rejects.toThrow();
    });

    it('throws ZodError for short password', async () => {
      await expect(
        service.create({ email: 'a@b.com', password: 'short' }),
      ).rejects.toThrow();
    });

    it('throws ZodError for missing email', async () => {
      await expect(
        service.create({ password: 'securePass123' }),
      ).rejects.toThrow();
    });

    it('throws ZodError for password exceeding max length', async () => {
      await expect(
        service.create({ email: 'a@b.com', password: 'x'.repeat(129) }),
      ).rejects.toThrow();
    });
  });

  // ── getById ───────────────────────────────────────────────

  describe('getById', () => {
    it('returns user when found', async () => {
      const user = makeSafeUser();
      vi.mocked(repo.findById).mockResolvedValue(user);

      const result = await service.getById('user-1');
      expect(result.email).toBe('alice@example.com');
      expect(repo.findById).toHaveBeenCalledWith('user-1');
    });

    it('throws NotFoundError when not found', async () => {
      await expect(service.getById('missing')).rejects.toThrow(NotFoundError);
    });
  });

  // ── getByEmail ────────────────────────────────────────────

  describe('getByEmail', () => {
    it('returns user when found', async () => {
      const user = makeSafeUser();
      vi.mocked(repo.findByEmail).mockResolvedValue(user);

      const result = await service.getByEmail('alice@example.com');
      expect(result.email).toBe('alice@example.com');
      expect(repo.findByEmail).toHaveBeenCalledWith('alice@example.com');
    });

    it('throws NotFoundError when not found', async () => {
      await expect(service.getByEmail('nobody@example.com')).rejects.toThrow(NotFoundError);
    });
  });

  // ── delete ────────────────────────────────────────────────

  describe('delete', () => {
    it('deletes user by id', async () => {
      vi.mocked(repo.findById).mockResolvedValue(makeSafeUser());

      await service.delete('user-1');
      expect(repo.delete).toHaveBeenCalledWith('user-1');
    });

    it('throws NotFoundError when user does not exist', async () => {
      await expect(service.delete('missing')).rejects.toThrow(NotFoundError);
    });
  });

  // ── count ─────────────────────────────────────────────────

  describe('count', () => {
    it('returns 0 when no users', async () => {
      const result = await service.count();
      expect(result).toBe(0);
    });

    it('returns 1 when one user exists', async () => {
      vi.mocked(repo.count).mockResolvedValue(1);
      const result = await service.count();
      expect(result).toBe(1);
    });

    it('returns correct count for multiple users', async () => {
      vi.mocked(repo.count).mockResolvedValue(5);
      const result = await service.count();
      expect(result).toBe(5);
    });
  });
});
