import { BadRequestException, ConflictException } from '@nestjs/common';

import { ContractTransitionService } from './contract-transition.service';

/**
 * Signing is one-shot. The single exception: a contract signed before company
 * stamps were required must be re-signable so the customer can attach one —
 * otherwise counterSign's both-stamps gate strands it forever. These specs pin
 * that exception open and pin everything else shut.
 */
describe('customer re-sign to attach a missing stamp', () => {
  const contractReady = { id: 'c-1', reference: 'CTR-1', status: 'CONTRACT_READY' };
  const signedNoStamp = { id: 'c-1', reference: 'CTR-1', status: 'SIGNED_CUSTOMER' };

  const build = (contract: unknown, existingSignature: unknown) => {
    const applied: unknown[] = [];
    const service = Object.create(
      ContractTransitionService.prototype,
    ) as ContractTransitionService;
    Object.assign(service, {
      contractsService: {
        findById: jest.fn().mockResolvedValue(contract),
        assertCustomerCanAccessContract: jest.fn().mockResolvedValue(undefined),
      },
      contractsRepository: {
        findSignature: jest.fn().mockResolvedValue(existingSignature),
        update: jest.fn().mockResolvedValue(undefined),
      },
      otpService: {
        verifyOtpForAction: jest.fn().mockResolvedValue(undefined),
        sendOtp: jest.fn().mockResolvedValue(undefined),
      },
      notifier: { customerSignedToStaff: jest.fn() },
      resolveSignerContacts: jest.fn().mockResolvedValue({ phone: '+251900000000' }),
      applySignature: jest.fn((...args: unknown[]) => {
        applied.push(args);
        return Promise.resolve();
      }),
      regenerateContractPdf: jest.fn().mockResolvedValue(undefined),
    });
    return { service, applied };
  };

  const dto = {
    role: 'CUSTOMER' as const,
    signerDisplayName: 'C. Customer',
    signatureImageBase64: 'data:image/png;base64,AAAA',
    stampImageBase64: 'data:image/png;base64,BBBB',
    otp: '123456',
  };

  it('lets a customer sign again when their signature has no stamp', async () => {
    const { service, applied } = build(signedNoStamp, {
      id: 's-1',
      role: 'CUSTOMER',
      stampFileId: null,
    });

    await expect(service.sign('c-1', dto, { signerUserId: 'u-1' })).resolves.toBeDefined();
    expect(applied).toHaveLength(1);
  });

  it('still refuses a second signature once a stamp is on file', async () => {
    const { service } = build(signedNoStamp, {
      id: 's-1',
      role: 'CUSTOMER',
      stampFileId: 'file-1',
    });

    // Stamped already → not the re-sign case, so the status guard rejects
    // SIGNED_CUSTOMER before the already-signed check is reached.
    await expect(service.sign('c-1', dto, { signerUserId: 'u-1' })).rejects.toBeInstanceOf(
      ConflictException,
    );
  });

  it('refuses a second signature on a still-ready contract', async () => {
    const { service } = build(contractReady, {
      id: 's-1',
      role: 'CUSTOMER',
      stampFileId: 'file-1',
    });

    await expect(service.sign('c-1', dto, { signerUserId: 'u-1' })).rejects.toThrow(
      /already signed/i,
    );
  });

  it('signs normally when nothing is on file yet', async () => {
    const { service, applied } = build(contractReady, null);

    await expect(service.sign('c-1', dto, { signerUserId: 'u-1' })).resolves.toBeDefined();
    expect(applied).toHaveLength(1);
  });

  it('sends a signing OTP for the stamp re-sign', async () => {
    const { service } = build(signedNoStamp, {
      id: 's-1',
      role: 'CUSTOMER',
      stampFileId: null,
    });

    await expect(
      service.sendSigningOtp('c-1', { signerUserId: 'u-1' }),
    ).resolves.toEqual(expect.objectContaining({ sentTo: expect.any(String) }));
  });

  it('refuses a signing OTP once the contract is signed and stamped', async () => {
    const { service } = build(signedNoStamp, {
      id: 's-1',
      role: 'CUSTOMER',
      stampFileId: 'file-1',
    });

    await expect(
      service.sendSigningOtp('c-1', { signerUserId: 'u-1' }),
    ).rejects.toBeInstanceOf(ConflictException);
  });

  it('requires the OTP on the re-sign path too', async () => {
    const { service } = build(signedNoStamp, {
      id: 's-1',
      role: 'CUSTOMER',
      stampFileId: null,
    });

    await expect(
      service.sign('c-1', { ...dto, otp: undefined }, { signerUserId: 'u-1' }),
    ).rejects.toBeInstanceOf(BadRequestException);
  });
});
