import { BadRequestException, ConflictException } from '@nestjs/common';
import { DataService } from './data.service';

describe('DataService', () => {
  const makeService = () => {
    const prisma = {
      appData: {
        create: jest.fn(),
        findUnique: jest.fn(),
        updateMany: jest.fn(),
        upsert: jest.fn(),
      },
    };
    const syncService = {
      syncAfterAppDataPut: jest.fn(),
    };
    return {
      prisma,
      syncService,
      service: new DataService(prisma as any, syncService as any),
    };
  };

  it('returns null for an allowed key with no stored value', async () => {
    const { prisma, service } = makeService();
    prisma.appData.findUnique.mockResolvedValue(null);

    await expect(service.get('products-overview')).resolves.toEqual({
      key: 'products-overview',
      value: null,
      version: null,
    });
  });

  it('returns the AppData version for a stored value', async () => {
    const { prisma, service } = makeService();
    const updatedAt = new Date('2026-07-02T08:00:00.000Z');
    prisma.appData.findUnique.mockResolvedValue({
      key: 'products-overview',
      value: [{ id: 'p1' }],
      updatedAt,
    });

    await expect(service.get('products-overview')).resolves.toEqual({
      key: 'products-overview',
      value: [{ id: 'p1' }],
      version: updatedAt.toISOString(),
    });
  });

  it('rejects unknown keys', async () => {
    const { service } = makeService();

    await expect(service.get('unknown-key')).rejects.toBeInstanceOf(BadRequestException);
  });

  it('upserts JSON values for allowed keys', async () => {
    const { prisma, service, syncService } = makeService();
    const value = [{ id: 'p1', name: 'Product 1' }];
    const updatedAt = new Date('2026-07-02T08:01:00.000Z');
    prisma.appData.upsert.mockResolvedValue({ key: 'products-overview', value, updatedAt });

    await expect(service.put('products-overview', value)).resolves.toEqual({
      key: 'products-overview',
      value,
      version: updatedAt.toISOString(),
    });
    expect(prisma.appData.upsert).toHaveBeenCalledWith({
      where: { key: 'products-overview' },
      update: { value },
      create: { key: 'products-overview', value },
    });
    expect(syncService.syncAfterAppDataPut).toHaveBeenCalledWith('products-overview');
  });

  it('allows supporting business data keys migrated from browser storage', async () => {
    const { prisma, service } = makeService();
    const value: unknown[] = [];
    prisma.appData.upsert.mockImplementation(({ where }) =>
      Promise.resolve({ key: where.key, value, updatedAt: new Date('2026-07-02T08:02:00.000Z') }),
    );

    await expect(service.put('task-worklogs', value)).resolves.toMatchObject({ key: 'task-worklogs', value });
    await expect(service.put('work-activities', value)).resolves.toMatchObject({ key: 'work-activities', value });
    await expect(service.put('overtime', { records: [], reasons: [] })).resolves.toMatchObject({
      key: 'overtime',
      value,
    });
  });

  it('updates only when the supplied version matches the stored row version', async () => {
    const { prisma, service, syncService } = makeService();
    const previousVersion = '2026-07-02T08:03:00.000Z';
    const nextUpdatedAt = new Date('2026-07-02T08:04:00.000Z');
    const nextValue = [{ id: 'p2', name: 'Product 2' }];
    prisma.appData.updateMany.mockResolvedValue({ count: 1 });
    prisma.appData.findUnique.mockResolvedValue({
      key: 'products-overview',
      value: nextValue,
      updatedAt: nextUpdatedAt,
    });

    await expect(service.put('products-overview', nextValue, previousVersion)).resolves.toEqual({
      key: 'products-overview',
      value: nextValue,
      version: nextUpdatedAt.toISOString(),
    });
    expect(prisma.appData.updateMany).toHaveBeenCalledWith({
      where: {
        key: 'products-overview',
        updatedAt: new Date(previousVersion),
      },
      data: { value: nextValue },
    });
    expect(syncService.syncAfterAppDataPut).toHaveBeenCalledWith('products-overview');
  });

  it('rejects stale AppData versions without overwriting the current value', async () => {
    const { prisma, service, syncService } = makeService();
    prisma.appData.updateMany.mockResolvedValue({ count: 0 });
    prisma.appData.findUnique.mockResolvedValue({
      key: 'products-overview',
      value: [{ id: 'current' }],
      updatedAt: new Date('2026-07-02T08:05:00.000Z'),
    });

    await expect(
      service.put('products-overview', [{ id: 'stale' }], '2026-07-02T08:03:00.000Z'),
    ).rejects.toBeInstanceOf(ConflictException);
    expect(prisma.appData.upsert).not.toHaveBeenCalled();
    expect(syncService.syncAfterAppDataPut).not.toHaveBeenCalled();
  });

  it('creates a missing row only when the client loaded a null version', async () => {
    const { prisma, service, syncService } = makeService();
    const value = [{ id: 'p1' }];
    const updatedAt = new Date('2026-07-02T08:06:00.000Z');
    prisma.appData.create.mockResolvedValue({ key: 'products-overview', value, updatedAt });

    await expect(service.put('products-overview', value, null)).resolves.toEqual({
      key: 'products-overview',
      value,
      version: updatedAt.toISOString(),
    });
    expect(prisma.appData.create).toHaveBeenCalledWith({
      data: { key: 'products-overview', value },
    });
    expect(prisma.appData.upsert).not.toHaveBeenCalled();
    expect(syncService.syncAfterAppDataPut).toHaveBeenCalledWith('products-overview');
  });

  it('rejects create-only writes when another client created the row first', async () => {
    const { prisma, service, syncService } = makeService();
    prisma.appData.create.mockRejectedValue({ code: 'P2002' });
    prisma.appData.findUnique.mockResolvedValue({
      key: 'products-overview',
      value: [{ id: 'current' }],
      updatedAt: new Date('2026-07-02T08:07:00.000Z'),
    });

    await expect(service.put('products-overview', [{ id: 'new' }], null)).rejects.toBeInstanceOf(
      ConflictException,
    );
    expect(syncService.syncAfterAppDataPut).not.toHaveBeenCalled();
  });

  it('rejects invalid AppData versions', async () => {
    const { service } = makeService();

    await expect(service.put('products-overview', [], 'not-a-date')).rejects.toBeInstanceOf(
      BadRequestException,
    );
  });

  it('keeps the AppData response successful when relation sync fails', async () => {
    const { prisma, service, syncService } = makeService();
    const value = [{ id: 'p1' }];
    const updatedAt = new Date('2026-07-02T08:08:00.000Z');
    const warnSpy = jest.spyOn((service as any).logger, 'warn').mockImplementation();
    prisma.appData.upsert.mockResolvedValue({ key: 'products-overview', value, updatedAt });
    syncService.syncAfterAppDataPut.mockRejectedValue(new Error('sync failed'));

    await expect(service.put('products-overview', value)).resolves.toEqual({
      key: 'products-overview',
      value,
      version: updatedAt.toISOString(),
    });
    expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('products-overview'));
  });
});
