import {
  BadRequestException,
  ConflictException,
  NotFoundException,
} from '@nestjs/common';
import { TripInvitationsService } from './trip-invitations.service';

describe('TripInvitationsService.acceptInvitation', () => {
  const future = new Date(Date.now() + 60_000);
  const past = new Date(Date.now() - 60_000);

  it('rejects accepting the same invitation twice', async () => {
    const repo = {
      findByTokenHash: jest.fn().mockResolvedValue({
        id: 'inv-1',
        tripId: 't1',
        expiresAt: future,
        acceptedAt: new Date(),
      }),
    };
    const members = { upsertActiveMember: jest.fn() };
    const service = new TripInvitationsService(repo as never, members as never);

    await expect(
      service.acceptInvitation('raw-token', 'user-3'),
    ).rejects.toThrow(ConflictException);
    expect(members.upsertActiveMember).not.toHaveBeenCalled();
  });

  it('rejects an expired invitation', async () => {
    const repo = {
      findByTokenHash: jest.fn().mockResolvedValue({
        id: 'inv-1',
        tripId: 't1',
        expiresAt: past,
        acceptedAt: null,
      }),
    };
    const members = { upsertActiveMember: jest.fn() };
    const service = new TripInvitationsService(repo as never, members as never);

    await expect(
      service.acceptInvitation('raw-token', 'user-2'),
    ).rejects.toThrow(BadRequestException);
    expect(members.upsertActiveMember).not.toHaveBeenCalled();
  });

  it('rejects an unknown token without leaking whether the trip exists', async () => {
    const repo = { findByTokenHash: jest.fn().mockResolvedValue(undefined) };
    const members = { upsertActiveMember: jest.fn() };
    const service = new TripInvitationsService(repo as never, members as never);

    await expect(
      service.acceptInvitation('does-not-exist', 'user-2'),
    ).rejects.toThrow(NotFoundException);
  });

  it('accepts a fresh, unexpired invitation and activates trip membership', async () => {
    const repo = {
      findByTokenHash: jest.fn().mockResolvedValue({
        id: 'inv-1',
        tripId: 't1',
        expiresAt: future,
        acceptedAt: null,
      }),
      markAccepted: jest.fn().mockResolvedValue(undefined),
    };
    const members = {
      upsertActiveMember: jest
        .fn()
        .mockResolvedValue({ id: 'm1', tripId: 't1', userId: 'user-2' }),
    };
    const service = new TripInvitationsService(repo as never, members as never);

    const member = await service.acceptInvitation('raw-token', 'user-2');

    expect(repo.markAccepted).toHaveBeenCalledWith('inv-1');
    expect(members.upsertActiveMember).toHaveBeenCalledWith(
      't1',
      'user-2',
      'MEMBER',
    );
    expect(member).toEqual({ id: 'm1', tripId: 't1', userId: 'user-2' });
  });
});

describe('TripInvitationsService.createInvitation', () => {
  it('stores only a hash of the generated token and returns the raw token once', async () => {
    const repo = {
      create: jest
        .fn()
        .mockImplementation((tripId: string, fields: Record<string, unknown>) =>
          Promise.resolve({ id: 'inv-1', tripId, ...fields }),
        ),
    };
    const members = { upsertActiveMember: jest.fn() };
    const service = new TripInvitationsService(repo as never, members as never);

    const result = await service.createInvitation(
      't1',
      'owner-1',
      'friend@example.com',
    );

    expect(result.rawToken).toEqual(expect.any(String));
    expect(repo.create).toHaveBeenCalledWith(
      't1',
      expect.objectContaining({
        email: 'friend@example.com',
        invitedByUserId: 'owner-1',
      }),
    );
    const [, fields] = repo.create.mock.calls[0] as [
      string,
      { tokenHash: string },
    ];
    expect(fields.tokenHash).not.toBe(result.rawToken);
  });
});
