import { describe, it, expect, vi, beforeEach } from 'vitest';
import { GroupService } from '../src/services/group.service.js';
import { NotFoundError, ConflictError } from '../src/services/mcp-server.service.js';
import type { IGroupRepository, GroupWithMembers } from '../src/repositories/group.repository.js';
import type { IUserRepository, SafeUser } from '../src/repositories/user.repository.js';
import type { Group } from '@prisma/client';

function makeGroup(overrides: Partial<Group> = {}): Group {
  return {
    id: 'grp-1',
    name: 'developers',
    description: 'Dev team',
    version: 1,
    createdAt: new Date(),
    updatedAt: new Date(),
    ...overrides,
  };
}

function makeGroupWithMembers(overrides: Partial<Group> = {}, members: GroupWithMembers['members'] = []): GroupWithMembers {
  return {
    ...makeGroup(overrides),
    members,
  };
}

function makeUser(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 mockGroupRepo(): IGroupRepository {
  return {
    findAll: vi.fn(async () => []),
    findById: vi.fn(async () => null),
    findByName: vi.fn(async () => null),
    create: vi.fn(async (data) => makeGroup({ name: data.name, description: data.description ?? '' })),
    update: vi.fn(async (id, data) => makeGroup({ id, description: data.description ?? '' })),
    delete: vi.fn(async () => {}),
    setMembers: vi.fn(async () => {}),
    findGroupsForUser: vi.fn(async () => []),
  };
}

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

describe('GroupService', () => {
  let groupRepo: ReturnType<typeof mockGroupRepo>;
  let userRepo: ReturnType<typeof mockUserRepo>;
  let service: GroupService;

  beforeEach(() => {
    groupRepo = mockGroupRepo();
    userRepo = mockUserRepo();
    service = new GroupService(groupRepo, userRepo);
  });

  describe('list', () => {
    it('returns empty list', async () => {
      const result = await service.list();
      expect(result).toEqual([]);
      expect(groupRepo.findAll).toHaveBeenCalled();
    });

    it('returns groups with members', async () => {
      const groups = [
        makeGroupWithMembers({ id: 'g1', name: 'admins' }, [
          { id: 'gm-1', user: { id: 'u1', email: 'a@b.com', name: 'A' } },
        ]),
      ];
      vi.mocked(groupRepo.findAll).mockResolvedValue(groups);
      const result = await service.list();
      expect(result).toHaveLength(1);
      expect(result[0].members).toHaveLength(1);
    });
  });

  describe('create', () => {
    it('creates a group without members', async () => {
      const created = makeGroupWithMembers({ name: 'my-group', description: '' }, []);
      vi.mocked(groupRepo.findById).mockResolvedValue(created);

      const result = await service.create({ name: 'my-group' });
      expect(result.name).toBe('my-group');
      expect(groupRepo.create).toHaveBeenCalledWith({ name: 'my-group', description: '' });
      expect(groupRepo.setMembers).not.toHaveBeenCalled();
    });

    it('creates a group with members', async () => {
      const alice = makeUser({ id: 'u-alice', email: 'alice@example.com' });
      const bob = makeUser({ id: 'u-bob', email: 'bob@example.com', name: 'Bob' });
      vi.mocked(userRepo.findByEmail).mockImplementation(async (email: string) => {
        if (email === 'alice@example.com') return alice;
        if (email === 'bob@example.com') return bob;
        return null;
      });

      const created = makeGroupWithMembers({ name: 'team' }, [
        { id: 'gm-1', user: { id: 'u-alice', email: 'alice@example.com', name: 'Alice' } },
        { id: 'gm-2', user: { id: 'u-bob', email: 'bob@example.com', name: 'Bob' } },
      ]);
      vi.mocked(groupRepo.findById).mockResolvedValue(created);

      const result = await service.create({
        name: 'team',
        members: ['alice@example.com', 'bob@example.com'],
      });

      expect(groupRepo.setMembers).toHaveBeenCalledWith('grp-1', ['u-alice', 'u-bob']);
      expect(result.members).toHaveLength(2);
    });

    it('throws ConflictError when name exists', async () => {
      vi.mocked(groupRepo.findByName).mockResolvedValue(makeGroupWithMembers({ name: 'taken' }));
      await expect(service.create({ name: 'taken' })).rejects.toThrow(ConflictError);
    });

    it('throws NotFoundError for unknown member email', async () => {
      vi.mocked(userRepo.findByEmail).mockResolvedValue(null);
      await expect(
        service.create({ name: 'team', members: ['unknown@example.com'] }),
      ).rejects.toThrow(NotFoundError);
    });

    it('validates input', async () => {
      await expect(service.create({ name: '' })).rejects.toThrow();
      await expect(service.create({ name: 'UPPERCASE' })).rejects.toThrow();
    });
  });

  describe('getById', () => {
    it('returns group when found', async () => {
      const group = makeGroupWithMembers({ id: 'g1' });
      vi.mocked(groupRepo.findById).mockResolvedValue(group);
      const result = await service.getById('g1');
      expect(result.id).toBe('g1');
    });

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

  describe('getByName', () => {
    it('returns group when found', async () => {
      const group = makeGroupWithMembers({ name: 'admins' });
      vi.mocked(groupRepo.findByName).mockResolvedValue(group);
      const result = await service.getByName('admins');
      expect(result.name).toBe('admins');
    });

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

  describe('update', () => {
    it('updates description', async () => {
      const group = makeGroupWithMembers({ id: 'g1' });
      vi.mocked(groupRepo.findById).mockResolvedValue(group);

      const updated = makeGroupWithMembers({ id: 'g1', description: 'new desc' });
      // After update, getById is called again to return fresh data
      vi.mocked(groupRepo.findById).mockResolvedValue(updated);

      const result = await service.update('g1', { description: 'new desc' });
      expect(groupRepo.update).toHaveBeenCalledWith('g1', { description: 'new desc' });
      expect(result.description).toBe('new desc');
    });

    it('updates members (full replacement)', async () => {
      const group = makeGroupWithMembers({ id: 'g1' }, [
        { id: 'gm-1', user: { id: 'u-old', email: 'old@example.com', name: 'Old' } },
      ]);
      vi.mocked(groupRepo.findById).mockResolvedValue(group);

      const alice = makeUser({ id: 'u-alice', email: 'alice@example.com' });
      vi.mocked(userRepo.findByEmail).mockResolvedValue(alice);

      const updated = makeGroupWithMembers({ id: 'g1' }, [
        { id: 'gm-2', user: { id: 'u-alice', email: 'alice@example.com', name: 'Alice' } },
      ]);
      vi.mocked(groupRepo.findById).mockResolvedValueOnce(group).mockResolvedValue(updated);

      const result = await service.update('g1', { members: ['alice@example.com'] });
      expect(groupRepo.setMembers).toHaveBeenCalledWith('g1', ['u-alice']);
      expect(result.members).toHaveLength(1);
    });

    it('throws NotFoundError when group not found', async () => {
      await expect(service.update('missing', { description: 'x' })).rejects.toThrow(NotFoundError);
    });

    it('throws NotFoundError for unknown member email on update', async () => {
      const group = makeGroupWithMembers({ id: 'g1' });
      vi.mocked(groupRepo.findById).mockResolvedValue(group);
      vi.mocked(userRepo.findByEmail).mockResolvedValue(null);

      await expect(
        service.update('g1', { members: ['unknown@example.com'] }),
      ).rejects.toThrow(NotFoundError);
    });
  });

  describe('delete', () => {
    it('deletes group', async () => {
      const group = makeGroupWithMembers({ id: 'g1' });
      vi.mocked(groupRepo.findById).mockResolvedValue(group);
      await service.delete('g1');
      expect(groupRepo.delete).toHaveBeenCalledWith('g1');
    });

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

  describe('group includes resolved member info', () => {
    it('members include user id, email, and name', async () => {
      const group = makeGroupWithMembers({ id: 'g1', name: 'team' }, [
        { id: 'gm-1', user: { id: 'u1', email: 'alice@example.com', name: 'Alice' } },
        { id: 'gm-2', user: { id: 'u2', email: 'bob@example.com', name: null } },
      ]);
      vi.mocked(groupRepo.findById).mockResolvedValue(group);

      const result = await service.getById('g1');
      expect(result.members[0].user).toEqual({ id: 'u1', email: 'alice@example.com', name: 'Alice' });
      expect(result.members[1].user).toEqual({ id: 'u2', email: 'bob@example.com', name: null });
    });
  });
});
