import { Test, TestingModule } from '@nestjs/testing';
import { BadRequestException, ForbiddenException, NotFoundException } from '@nestjs/common';
import { CheckinService } from './checkin.service';
import { PrismaService } from '../database/prisma.service';
import { AuditService } from '../audit/audit.service';
import { RequestUser } from '../common/request-user';
import { VisitorPassStatus, VisitSessionStatus, UserRole } from '@prisma/client';

describe('CheckinService', () => {
  let service: CheckinService;
  let audit: AuditService;

  const mockPrisma = {
    visitorPass: {
      findFirst: jest.fn(),
      update: jest.fn()
    },
    visitSession: {
      create: jest.fn(),
      findUnique: jest.fn(),
      update: jest.fn()
    },
    guard: {
      findFirst: jest.fn(),
      update: jest.fn()
    },
    estateGate: {
      findFirst: jest.fn()
    },
    appNotification: {
      create: jest.fn()
    },
    offlineSyncItem: {
      findUnique: jest.fn(),
      update: jest.fn()
    },
    $transaction: jest.fn()
  };

  const mockAudit = {
    record: jest.fn()
  };

  const completeGuard = (overrides = {}) => ({
    id: 'guard-1',
    estateId: 'estate-1',
    userId: 'guard-1',
    status: 'ACTIVE',
    phone: '+2348000000004',
    deviceId: null,
    guardCode: 'GRD-TEST',
    firstName: 'Main',
    lastName: 'Guard',
    address: 'Main gate post',
    profilePictureUrl: 'https://example.com/guard.jpg',
    emergencyContact: { name: 'Supervisor', phone: '+2348000000002' },
    assignedGateId: 'gate-1',
    assignedShifts: [{ day: 'DAILY', startsAt: '08:00', endsAt: '18:00' }],
    profileConfirmed: true,
    ...overrides
  });

  const passContext = {
    visitor: { id: 'visitor-1', name: 'John Doe', phone: '+2348111111111' },
    hostResident: { id: 'resident-1', userId: 'resident-user-1', name: 'Ada Okafor' }
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        CheckinService,
        { provide: PrismaService, useValue: mockPrisma },
        { provide: AuditService, useValue: mockAudit }
      ]
    }).compile();

    service = module.get<CheckinService>(CheckinService);
    audit = module.get<AuditService>(AuditService);

    jest.clearAllMocks();
    mockPrisma.guard.findFirst.mockResolvedValue(completeGuard());
    mockPrisma.estateGate.findFirst.mockResolvedValue({ id: 'gate-1' });
    mockPrisma.appNotification.create.mockResolvedValue({});
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });

  describe('checkinByQr', () => {
    it('should check in visitor with valid QR token', async () => {
      const user: RequestUser = {
        id: 'guard-1',
        role: UserRole.GUARD,
        estateId: 'estate-1'
      };

      const dto = {
        estateId: 'estate-1',
        qrToken: 'token-123',
        guardId: 'guard-1',
        entryGate: 'Main Gate'
      };

      const mockPass = {
        id: 'pass-1',
        estateId: 'estate-1',
        visitorCode: 'ABC12345',
        status: VisitorPassStatus.ACTIVE,
        validFrom: new Date(Date.now() - 1000),
        validUntil: new Date(Date.now() + 86400000),
        maxEntries: 1,
        entryCount: 0,
        visitorId: 'visitor-1',
        hostResidentId: 'resident-1',
        ...passContext
      };

      const mockSession = {
        id: 'session-1',
        estateId: 'estate-1',
        visitorPassId: 'pass-1',
        guardId: 'guard-1',
        checkinTime: new Date(),
        checkoutTime: null,
        entryGate: 'Main Gate',
        exitGate: null,
        status: VisitSessionStatus.CHECKED_IN,
        offlineEventId: null,
        createdAt: new Date(),
        updatedAt: new Date()
      };

      mockPrisma.visitorPass.findFirst.mockResolvedValue(mockPass);
      mockPrisma.$transaction.mockImplementation(async (callback: any) => {
        return callback({
          visitSession: { create: async () => ({ ...mockSession, visitorPass: passContext }) },
          visitorPass: {
            updateMany: async () => ({ count: 1 }),
            findUniqueOrThrow: async () => ({ entryCount: 1, maxEntries: 1 }),
            update: async () => mockPass
          }
        });
      });
      mockAudit.record.mockResolvedValue(undefined);

      const result = await service.checkinByQr(dto, user);

      expect(result).toMatchObject({
        status: VisitSessionStatus.CHECKED_IN
      });
      expect(audit.record).toHaveBeenCalled();
    });

    it('should throw error for expired pass', async () => {
      const user: RequestUser = {
        id: 'guard-1',
        role: UserRole.GUARD,
        estateId: 'estate-1'
      };

      const dto = {
        estateId: 'estate-1',
        qrToken: 'token-123',
        guardId: 'guard-1',
        entryGate: 'Main Gate'
      };

      const mockPass = {
        id: 'pass-1',
        estateId: 'estate-1',
        visitorCode: 'ABC12345',
        status: VisitorPassStatus.ACTIVE,
        validFrom: new Date(Date.now() - 86400000),
        validUntil: new Date(Date.now() - 1000),
        maxEntries: 1,
        entryCount: 0,
        visitorId: 'visitor-1',
        hostResidentId: 'resident-1',
        ...passContext
      };

      mockPrisma.visitorPass.findFirst.mockResolvedValue(mockPass);

      await expect(service.checkinByQr(dto, user)).rejects.toThrow(BadRequestException);
    });

    it('should throw error for non-existent pass', async () => {
      const user: RequestUser = {
        id: 'guard-1',
        role: UserRole.GUARD,
        estateId: 'estate-1'
      };

      const dto = {
        estateId: 'estate-1',
        qrToken: 'non-existent',
        guardId: 'guard-1',
        entryGate: 'Main Gate'
      };

      mockPrisma.visitorPass.findFirst.mockResolvedValue(null);

      await expect(service.checkinByQr(dto, user)).rejects.toThrow(NotFoundException);
    });

    it('should throw error for revoked pass', async () => {
      const user: RequestUser = {
        id: 'guard-1',
        role: UserRole.GUARD,
        estateId: 'estate-1'
      };

      const dto = {
        estateId: 'estate-1',
        qrToken: 'token-123',
        guardId: 'guard-1',
        entryGate: 'Main Gate'
      };

      const mockPass = {
        id: 'pass-1',
        estateId: 'estate-1',
        visitorCode: 'ABC12345',
        status: VisitorPassStatus.REVOKED,
        validFrom: new Date(Date.now() - 1000),
        validUntil: new Date(Date.now() + 86400000),
        maxEntries: 1,
        entryCount: 0,
        visitorId: 'visitor-1',
        hostResidentId: 'resident-1',
        ...passContext
      };

      mockPrisma.visitorPass.findFirst.mockResolvedValue(mockPass);

      await expect(service.checkinByQr(dto, user)).rejects.toThrow(BadRequestException);
    });

    it('should reject a guard acting with another guard id', async () => {
      const user: RequestUser = {
        id: 'guard-1',
        role: UserRole.GUARD,
        estateId: 'estate-1'
      };

      const dto = {
        estateId: 'estate-1',
        qrToken: 'token-123',
        guardId: 'guard-2',
        entryGate: 'Main Gate'
      };

      mockPrisma.guard.findFirst.mockResolvedValueOnce({
        id: 'guard-2',
        estateId: 'estate-1',
        userId: 'different-user',
        status: 'ACTIVE'
      });

      await expect(service.checkinByQr(dto, user)).rejects.toThrow(ForbiddenException);
    });

    it('should bind an unassigned guard to the first submitted scanner device', async () => {
      const user: RequestUser = {
        id: 'guard-1',
        role: UserRole.GUARD,
        estateId: 'estate-1'
      };

      const dto = {
        estateId: 'estate-1',
        qrToken: 'token-123',
        guardId: 'guard-1',
        entryGate: 'Main Gate',
        deviceId: 'gate-tablet-001'
      };

      const mockPass = {
        id: 'pass-1',
        estateId: 'estate-1',
        visitorCode: 'ABC12345',
        status: VisitorPassStatus.ACTIVE,
        validFrom: new Date(Date.now() - 1000),
        validUntil: new Date(Date.now() + 86400000),
        maxEntries: 1,
        entryCount: 0,
        visitorId: 'visitor-1',
        hostResidentId: 'resident-1',
        ...passContext
      };

      const mockSession = {
        id: 'session-1',
        estateId: 'estate-1',
        visitorPassId: 'pass-1',
        guardId: 'guard-1',
        deviceId: 'gate-tablet-001',
        checkinTime: new Date(),
        checkoutTime: null,
        entryGate: 'Main Gate',
        exitGate: null,
        status: VisitSessionStatus.CHECKED_IN,
        offlineEventId: null,
        createdAt: new Date(),
        updatedAt: new Date()
      };

      mockPrisma.guard.update.mockResolvedValueOnce(completeGuard({ deviceId: 'gate-tablet-001' }));
      mockPrisma.visitorPass.findFirst.mockResolvedValue(mockPass);
      mockPrisma.$transaction.mockImplementation(async (callback: any) => {
        return callback({
          visitSession: { create: async () => ({ ...mockSession, visitorPass: passContext }) },
          visitorPass: {
            updateMany: async () => ({ count: 1 }),
            findUniqueOrThrow: async () => ({ entryCount: 1, maxEntries: 1 }),
            update: async () => mockPass
          }
        });
      });
      mockAudit.record.mockResolvedValue(undefined);

      const result = await service.checkinByQr(dto, user);

      expect(result).toMatchObject({ deviceId: 'gate-tablet-001' });
      expect(mockPrisma.guard.update).toHaveBeenCalledWith({
        where: { id: 'guard-1' },
        data: { deviceId: 'gate-tablet-001' }
      });
    });

    it('should reject scanner device mismatch for an assigned guard', async () => {
      const user: RequestUser = {
        id: 'guard-1',
        role: UserRole.GUARD,
        estateId: 'estate-1'
      };

      const dto = {
        estateId: 'estate-1',
        qrToken: 'token-123',
        guardId: 'guard-1',
        entryGate: 'Main Gate',
        deviceId: 'gate-tablet-999'
      };

      mockPrisma.guard.findFirst.mockResolvedValueOnce(completeGuard({ deviceId: 'gate-tablet-001' }));

      const result = service.checkinByQr(dto, user);

      await expect(result).rejects.toThrow(ForbiddenException);
      await expect(result).rejects.toThrow('Guard account is assigned to a different device');
    });
  });;

  describe('checkinByCode', () => {
    it('should check in visitor with manual code', async () => {
      const user: RequestUser = {
        id: 'guard-1',
        role: UserRole.GUARD,
        estateId: 'estate-1'
      };

      const dto = {
        estateId: 'estate-1',
        visitorCode: 'ABC12345',
        guardId: 'guard-1',
        entryGate: 'Main Gate'
      };

      const mockPass = {
        id: 'pass-1',
        estateId: 'estate-1',
        visitorCode: 'ABC12345',
        status: VisitorPassStatus.ACTIVE,
        validFrom: new Date(Date.now() - 1000),
        validUntil: new Date(Date.now() + 86400000),
        maxEntries: 1,
        entryCount: 0,
        visitorId: 'visitor-1',
        hostResidentId: 'resident-1',
        ...passContext
      };

      const mockSession = {
        id: 'session-1',
        estateId: 'estate-1',
        visitorPassId: 'pass-1',
        guardId: 'guard-1',
        checkinTime: new Date(),
        checkoutTime: null,
        entryGate: 'Main Gate',
        exitGate: null,
        status: VisitSessionStatus.CHECKED_IN,
        offlineEventId: null,
        createdAt: new Date(),
        updatedAt: new Date()
      };

      mockPrisma.visitorPass.findFirst.mockResolvedValue(mockPass);
      mockPrisma.$transaction.mockImplementation(async (callback: any) => {
        return callback({
          visitSession: { create: async () => ({ ...mockSession, visitorPass: passContext }) },
          visitorPass: {
            updateMany: async () => ({ count: 1 }),
            findUniqueOrThrow: async () => ({ entryCount: 1, maxEntries: 1 }),
            update: async () => mockPass
          }
        });
      });
      mockAudit.record.mockResolvedValue(undefined);

      const result = await service.checkinByCode(dto, user);

      expect(result).toMatchObject({
        status: VisitSessionStatus.CHECKED_IN
      });
    });
  });

  describe('checkout', () => {
    it('should checkout visitor', async () => {
      const user: RequestUser = {
        id: 'guard-1',
        role: UserRole.GUARD,
        estateId: 'estate-1'
      };

      const dto = {
        visitSessionId: 'session-1',
        guardId: 'guard-1',
        exitGate: 'Main Gate'
      };

      const mockSession = {
        id: 'session-1',
        estateId: 'estate-1',
        visitorPassId: 'pass-1',
        guardId: 'guard-1',
        checkinTime: new Date(),
        checkoutTime: new Date(),
        entryGate: 'Main Gate',
        exitGate: 'Main Gate',
        status: VisitSessionStatus.CHECKED_IN,
        offlineEventId: null,
        createdAt: new Date(),
        updatedAt: new Date()
      };

      const updatedSession = {
        ...mockSession,
        visitorPass: passContext,
        checkoutTime: new Date(),
        status: VisitSessionStatus.CHECKED_OUT
      };

      mockPrisma.visitSession.findUnique.mockResolvedValue(mockSession);
      mockPrisma.visitSession.update.mockResolvedValue(updatedSession);
      mockAudit.record.mockResolvedValue(undefined);

      const result = await service.checkout(dto, user);

      expect(result).toMatchObject({
        status: VisitSessionStatus.CHECKED_OUT
      });
      expect(audit.record).toHaveBeenCalled();
    });

    it('should throw error for non-existent session', async () => {
      const user: RequestUser = {
        id: 'guard-1',
        role: UserRole.GUARD,
        estateId: 'estate-1'
      };

      const dto = {
        visitSessionId: 'non-existent',
        guardId: 'guard-1',
        exitGate: 'Main Gate'
      };

      mockPrisma.visitSession.findUnique.mockResolvedValue(null);

      await expect(service.checkout(dto, user)).rejects.toThrow(NotFoundException);
    });
  });
});
