import { BadRequestException, ForbiddenException } from '@nestjs/common';
import type { PrismaService } from '@/prisma/prisma.service';
import type { ResendMailPlenaryService } from '../mail/resend-mail-plenary.service';
import { RgcDecisionService } from './rgc-decision.service';

jest.mock('@/generated/prisma/client', () => ({ PlenaryStatus: {} }));
jest.mock('@/prisma/prisma.service', () => ({ PrismaService: class {} }));
jest.mock('../mail/resend-mail-plenary.service', () => ({
  ResendMailPlenaryService: class {},
}));

describe('Ministry RGC revision permissions', () => {
  const submittedAt = new Date('2026-09-01T00:00:00Z');
  let service: RgcDecisionService;
  let decision: Record<string, unknown>;
  let db: {
    user: { findFirst: jest.Mock };
    stakeholderUser: { findMany: jest.Mock };
    plenaryRgcDecision: { findFirst: jest.Mock; update: jest.Mock };
  };

  function stub(name: string, value: unknown) {
    const mock = jest.fn().mockResolvedValue(value);
    Object.defineProperty(service, name, { value: mock, configurable: true });
    return mock;
  }

  beforeEach(() => {
    decision = {
      id: 1,
      plenaryId: 2,
      stakeholderId: 3,
      categoryId: 4,
      indicatorId: null,
      userId: 5,
      revisionOpen: false,
      submittedToCdcAt: submittedAt,
    };
    db = {
      user: {
        findFirst: jest.fn().mockResolvedValue({
          id: 5,
          name: 'User',
          email: 'user@example.test',
        }),
      },
      stakeholderUser: {
        findMany: jest.fn().mockResolvedValue([{ stakeholderId: 3 }]),
      },
      plenaryRgcDecision: {
        findFirst: jest
          .fn()
          .mockImplementation(() => Promise.resolve({ ...decision })),
        update: jest
          .fn()
          .mockImplementation(({ data }: { data: Record<string, unknown> }) => {
            Object.assign(decision, data);
            return Promise.resolve({ ...decision });
          }),
      },
    };
    service = new RgcDecisionService(
      db as unknown as PrismaService,
      {} as ResendMailPlenaryService,
    );
    stub('findOne', { id: 1, status: 'In Progress', category: 'Category' });
  });

  it('requires an active CDC GPSF role to open a revision', async () => {
    db.user.findFirst.mockResolvedValue(null);
    await expect(service.openRevision(1, 5)).rejects.toBeInstanceOf(
      ForbiddenException,
    );
    expect(db.user.findFirst).toHaveBeenCalledWith(
      expect.objectContaining({
        where: expect.objectContaining({
          roles: {
            some: {
              role: {
                name: { equals: 'cdc_g-psf', mode: 'insensitive' },
                deletedAt: null,
              },
            },
          },
        }),
      }),
    );
    expect(db.plenaryRgcDecision.update).not.toHaveBeenCalled();
  });

  it('preserves submission history when CDC opens a revision', async () => {
    await service.openRevision(1, 5);
    expect(decision.revisionOpen).toBe(true);
    expect(decision.submittedToCdcAt).toBe(submittedAt);
  });

  it('does not reopen drafts or CDC-created decisions', async () => {
    decision.submittedToCdcAt = null;
    await expect(service.openRevision(1, 5)).rejects.toBeInstanceOf(
      BadRequestException,
    );
    decision.submittedToCdcAt = submittedAt;
    db.stakeholderUser.findMany.mockResolvedValue([]);
    await expect(service.openRevision(1, 5)).rejects.toBeInstanceOf(
      BadRequestException,
    );
  });

  it('blocks edits and repeat submission until CDC opens a revision', async () => {
    await expect(
      service.update(1, { decision: 'Changed' }, 5),
    ).rejects.toBeInstanceOf(BadRequestException);
    await expect(service.submitToCdc(1, 5)).rejects.toBeInstanceOf(
      BadRequestException,
    );
    expect(db.plenaryRgcDecision.update).not.toHaveBeenCalled();
  });

  it('allows the owning ministry to save without closing the revision', async () => {
    decision.revisionOpen = true;
    await service.update(1, { decision: 'Changed' }, 5);
    expect(decision.decision).toBe('Changed');
    expect(decision.revisionOpen).toBe(true);
    expect(decision.submittedToCdcAt).toBe(submittedAt);
  });

  it('rejects another ministry and prevents ownership changes during revision', async () => {
    decision.revisionOpen = true;
    db.stakeholderUser.findMany.mockResolvedValue([{ stakeholderId: 99 }]);
    await expect(
      service.update(1, { decision: 'Changed' }, 5),
    ).rejects.toBeInstanceOf(ForbiddenException);
    await expect(service.submitToCdc(1, 5)).rejects.toBeInstanceOf(
      ForbiddenException,
    );
    db.stakeholderUser.findMany.mockResolvedValue([{ stakeholderId: 3 }]);
    await expect(
      service.update(1, { stakeholderId: 99 }, 5),
    ).rejects.toBeInstanceOf(BadRequestException);
    expect(db.plenaryRgcDecision.update).not.toHaveBeenCalled();
  });

  it('resubmits, closes permission, and locks further edits and deletion', async () => {
    decision.revisionOpen = true;
    stub('getCdcReceiverTargets', { stakeholderIds: [10], userIds: [] });
    stub('getCefpReceiverTargets', { stakeholderIds: [], userIds: [] });
    const notify = stub('createOrUpdateSubmitNotification', 'updated');
    stub('getCdcReceiverEmails', []);
    stub('sendSubmitToCdcEmailNotification', undefined);
    stub('hydrateDecisions', []);
    await service.submitToCdc(1, 5);
    expect(decision.revisionOpen).toBe(false);
    expect(decision.submittedToCdcAt).not.toBe(submittedAt);
    expect(notify).toHaveBeenCalled();
    await expect(
      service.update(1, { decision: 'Changed again' }, 5),
    ).rejects.toBeInstanceOf(BadRequestException);
    await expect(service.remove(1, 5)).rejects.toBeInstanceOf(
      BadRequestException,
    );
  });
});
