import { Test, TestingModule } from '@nestjs/testing';
import { BadRequestException } from '@nestjs/common';
import { IntegrationService } from './integration.service';
import { PrismaService } from '../../prisma/prisma.service';
import { SyncDirection } from './dto/find-sync-logs.dto';

jest.mock('@prisma/client', () => ({
  PrismaClient: class PrismaClient {},
  Prisma: {},
}));

describe('IntegrationService', () => {
  let service: IntegrationService;

  const mockPrisma = {
    integrationConfig: {
      findUnique: jest.fn(),
      create: jest.fn(),
      update: jest.fn(),
    },
    syncLog: {
      findMany: jest.fn(),
      create: jest.fn(),
    },
  };

  const mockSyncQueue = {
    add: jest.fn().mockResolvedValue({}),
  };

  const CLUB_ID = 'club-uuid-1';
  const CONFIG_ID = 'config-uuid-1';
  const SYNC_LOG_ID = 'synclog-uuid-1';

  const mockConfig = {
    id: CONFIG_ID,
    clubId: CLUB_ID,
    provider: '1c',
    apiUrl: 'https://1c.example.com/api',
    apiKey: 'my-secret-api-key-1234',
    syncEnabled: true,
    lastSyncAt: null,
    settings: {},
    createdAt: new Date('2026-01-01T10:00:00Z'),
    updatedAt: new Date('2026-01-01T10:00:00Z'),
  };

  const mockSyncLog = {
    id: SYNC_LOG_ID,
    clubId: CLUB_ID,
    integrationId: CONFIG_ID,
    direction: SyncDirection.INBOUND,
    entityType: 'clients',
    recordsProcessed: 0,
    recordsFailed: 0,
    startedAt: new Date('2026-01-01T10:00:00Z'),
    finishedAt: null,
    errorMessage: null,
    createdAt: new Date('2026-01-01T10:00:00Z'),
  };

  beforeEach(async () => {
    jest.clearAllMocks();

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        IntegrationService,
        { provide: PrismaService, useValue: mockPrisma },
        { provide: 'BullQueue_integration-sync', useValue: mockSyncQueue },
      ],
    }).compile();

    service = module.get<IntegrationService>(IntegrationService);
  });

  // ─── getConfig ────────────────────────────────────────────────────────────

  describe('getConfig', () => {
    it('returns masked config when a config record exists', async () => {
      mockPrisma.integrationConfig.findUnique.mockResolvedValue(mockConfig);

      const result = await service.getConfig(CLUB_ID);

      expect(mockPrisma.integrationConfig.findUnique).toHaveBeenCalledWith({
        where: { clubId: CLUB_ID },
      });
      // apiKey should be masked: *** + last 4 chars
      expect(result.apiKey).toBe('***1234');
      expect(result.apiUrl).toBe(mockConfig.apiUrl);
      expect(result.syncEnabled).toBe(true);
    });

    it('returns a default empty config when no config record exists', async () => {
      mockPrisma.integrationConfig.findUnique.mockResolvedValue(null);

      const result = await service.getConfig(CLUB_ID);

      expect(result).toEqual({
        clubId: CLUB_ID,
        provider: '1c',
        apiUrl: null,
        apiKey: null,
        syncEnabled: false,
        lastSyncAt: null,
        settings: null,
      });
    });

    it('returns null apiKey when config exists but apiKey is null', async () => {
      mockPrisma.integrationConfig.findUnique.mockResolvedValue({
        ...mockConfig,
        apiKey: null,
      });

      const result = await service.getConfig(CLUB_ID);

      expect(result.apiKey).toBeNull();
    });
  });

  // ─── upsertConfig ─────────────────────────────────────────────────────────

  describe('upsertConfig', () => {
    const updateDto = {
      apiUrl: 'https://new.1c.example.com/api',
      apiKey: 'new-api-key',
      syncEnabled: true,
    };

    it('updates existing config when a record already exists', async () => {
      mockPrisma.integrationConfig.findUnique.mockResolvedValue(mockConfig);
      const updated = { ...mockConfig, apiUrl: updateDto.apiUrl };
      mockPrisma.integrationConfig.update.mockResolvedValue(updated);

      const result = await service.upsertConfig(CLUB_ID, updateDto);

      expect(mockPrisma.integrationConfig.update).toHaveBeenCalledWith({
        where: { clubId: CLUB_ID },
        data: expect.objectContaining({
          apiUrl: updateDto.apiUrl,
          apiKey: updateDto.apiKey,
          syncEnabled: updateDto.syncEnabled,
        }),
      });
      expect(mockPrisma.integrationConfig.create).not.toHaveBeenCalled();
      expect(result).toEqual(updated);
    });

    it('creates a new config when no existing record is found', async () => {
      mockPrisma.integrationConfig.findUnique.mockResolvedValue(null);
      const created = { ...mockConfig, ...updateDto };
      mockPrisma.integrationConfig.create.mockResolvedValue(created);

      const result = await service.upsertConfig(CLUB_ID, updateDto);

      expect(mockPrisma.integrationConfig.create).toHaveBeenCalledWith({
        data: expect.objectContaining({
          clubId: CLUB_ID,
          provider: '1c',
          apiUrl: updateDto.apiUrl,
          apiKey: updateDto.apiKey,
          syncEnabled: updateDto.syncEnabled,
        }),
      });
      expect(mockPrisma.integrationConfig.update).not.toHaveBeenCalled();
      expect(result).toEqual(created);
    });

    it('defaults syncEnabled to false and settings to {} when not provided on create', async () => {
      mockPrisma.integrationConfig.findUnique.mockResolvedValue(null);
      mockPrisma.integrationConfig.create.mockResolvedValue(mockConfig);

      await service.upsertConfig(CLUB_ID, {
        apiUrl: 'https://1c.example.com',
        apiKey: 'key',
      });

      expect(mockPrisma.integrationConfig.create).toHaveBeenCalledWith({
        data: expect.objectContaining({
          syncEnabled: false,
          settings: {},
        }),
      });
    });
  });

  // ─── triggerSync ──────────────────────────────────────────────────────────

  describe('triggerSync', () => {
    const triggerDto = {
      entityType: 'clients',
      direction: SyncDirection.INBOUND,
    };

    it('creates a syncLog and returns started status when config is valid', async () => {
      mockPrisma.integrationConfig.findUnique.mockResolvedValue(mockConfig);
      mockPrisma.syncLog.create.mockResolvedValue(mockSyncLog);

      const result = await service.triggerSync(CLUB_ID, triggerDto);

      expect(mockPrisma.syncLog.create).toHaveBeenCalledWith({
        data: expect.objectContaining({
          clubId: CLUB_ID,
          integrationId: CONFIG_ID,
          direction: SyncDirection.INBOUND,
          entityType: 'clients',
          recordsProcessed: 0,
          recordsFailed: 0,
          startedAt: expect.any(Date),
        }),
      });
      expect(result.syncLogId).toBe(SYNC_LOG_ID);
      expect(result.status).toBe('started');
      expect(result.message).toContain('inbound');
      expect(result.message).toContain('clients');
    });

    it('throws BadRequestException when no config exists', async () => {
      mockPrisma.integrationConfig.findUnique.mockResolvedValue(null);

      await expect(service.triggerSync(CLUB_ID, triggerDto)).rejects.toThrow(
        BadRequestException,
      );
      expect(mockPrisma.syncLog.create).not.toHaveBeenCalled();
    });

    it('throws BadRequestException when syncEnabled is false', async () => {
      mockPrisma.integrationConfig.findUnique.mockResolvedValue({
        ...mockConfig,
        syncEnabled: false,
      });

      await expect(service.triggerSync(CLUB_ID, triggerDto)).rejects.toThrow(
        new BadRequestException(
          'Integration sync is not enabled. Configure and enable sync first.',
        ),
      );
      expect(mockPrisma.syncLog.create).not.toHaveBeenCalled();
    });

    it('throws BadRequestException when apiUrl is missing', async () => {
      mockPrisma.integrationConfig.findUnique.mockResolvedValue({
        ...mockConfig,
        apiUrl: null,
      });

      await expect(service.triggerSync(CLUB_ID, triggerDto)).rejects.toThrow(
        new BadRequestException(
          'Integration API URL and API key must be configured before sync.',
        ),
      );
    });

    it('throws BadRequestException when apiKey is missing', async () => {
      mockPrisma.integrationConfig.findUnique.mockResolvedValue({
        ...mockConfig,
        apiKey: null,
      });

      await expect(service.triggerSync(CLUB_ID, triggerDto)).rejects.toThrow(
        new BadRequestException(
          'Integration API URL and API key must be configured before sync.',
        ),
      );
    });
  });

  // ─── findSyncLogs ─────────────────────────────────────────────────────────

  describe('findSyncLogs', () => {
    it('returns paginated sync logs without filters', async () => {
      mockPrisma.syncLog.findMany.mockResolvedValue([mockSyncLog]);

      const result = await service.findSyncLogs(CLUB_ID, { limit: 20 });

      expect(mockPrisma.syncLog.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { clubId: CLUB_ID },
          take: 21,
          orderBy: { createdAt: 'desc' },
        }),
      );
      expect(result.data).toHaveLength(1);
      expect(result.hasMore).toBe(false);
      expect(result.nextCursor).toBeNull();
    });

    it('sets hasMore and nextCursor when there are more items than limit', async () => {
      const logs = Array.from({ length: 3 }, (_, i) => ({
        ...mockSyncLog,
        id: `log-${i}`,
      }));
      mockPrisma.syncLog.findMany.mockResolvedValue(logs);

      const result = await service.findSyncLogs(CLUB_ID, { limit: 2 });

      expect(result.hasMore).toBe(true);
      expect(result.data).toHaveLength(2);
      expect(result.nextCursor).toBe('log-1');
    });

    it('filters by direction when provided', async () => {
      mockPrisma.syncLog.findMany.mockResolvedValue([]);

      await service.findSyncLogs(CLUB_ID, {
        limit: 20,
        direction: SyncDirection.OUTBOUND,
      });

      expect(mockPrisma.syncLog.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: {
            clubId: CLUB_ID,
            direction: SyncDirection.OUTBOUND,
          },
        }),
      );
    });

    it('filters by entityType when provided', async () => {
      mockPrisma.syncLog.findMany.mockResolvedValue([]);

      await service.findSyncLogs(CLUB_ID, {
        limit: 20,
        entityType: 'trainings',
      });

      expect(mockPrisma.syncLog.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: {
            clubId: CLUB_ID,
            entityType: 'trainings',
          },
        }),
      );
    });

    it('applies cursor-based pagination when cursor is provided', async () => {
      mockPrisma.syncLog.findMany.mockResolvedValue([]);

      await service.findSyncLogs(CLUB_ID, {
        cursor: 'prev-log-id',
        limit: 10,
      });

      expect(mockPrisma.syncLog.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          cursor: { id: 'prev-log-id' },
          skip: 1,
        }),
      );
    });
  });
});
