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

import { PriorityConfig } from '../entities/priority-config.entity';
import { PriorityConfigsService } from './priority-configs.service';

/**
 * Contiguous-range rules for priority configs: per type (per currency for
 * CURRENCY), ranges run from 1 with no gaps and no overlaps; the next range
 * must start at the lowest uncovered wagon count. There is no upper ceiling.
 */
describe('PriorityConfigsService range validation', () => {
  const rule = (
    type: PriorityConfig['type'],
    min: number,
    max: number,
    currency: string | null = null,
    id = `${type}-${min}-${max}-${currency ?? 'none'}`,
  ): PriorityConfig =>
    ({
      id,
      type,
      label: `${min}-${max}`,
      currency,
      minWagonCount: min,
      maxWagonCount: max,
    }) as PriorityConfig;

  const serviceWith = (rules: PriorityConfig[]): PriorityConfigsService => {
    const repository = {
      findAll: jest.fn(async ({ where }: { where: { type: string } }) =>
        rules.filter((r) => r.type === where.type),
      ),
      findById: jest.fn(async (id: string) =>
        rules.find((r) => r.id === id) ?? null,
      ),
    };
    return new PriorityConfigsService(
      repository as never,
      undefined as never, // DisplayOrderService — unused by range validation
    );
  };

  const attempt = (
    svc: PriorityConfigsService,
    input: Partial<Parameters<PriorityConfigsService['assertNoRangeCollision']>[0]>,
  ) =>
    svc.assertNoRangeCollision({
      type: 'WAGON',
      minWagonCount: 1,
      maxWagonCount: 5,
      ...input,
    });

  it('accepts the first WAGON range starting at 1', async () => {
    await expect(
      attempt(serviceWith([]), { minWagonCount: 1, maxWagonCount: 5 }),
    ).resolves.toBeUndefined();
  });

  it('rejects a first range that does not start at 1', async () => {
    await expect(
      attempt(serviceWith([]), { minWagonCount: 3, maxWagonCount: 5 }),
    ).rejects.toThrow(BadRequestException);
  });

  it('rejects an exact duplicate (1–5 vs 1–5)', async () => {
    await expect(
      attempt(serviceWith([rule('WAGON', 1, 5)]), {
        minWagonCount: 1,
        maxWagonCount: 5,
      }),
    ).rejects.toThrow(/must start at 6/);
  });

  it('rejects a partial overlap (4–7 after 1–5)', async () => {
    await expect(
      attempt(serviceWith([rule('WAGON', 1, 5)]), {
        minWagonCount: 4,
        maxWagonCount: 7,
      }),
    ).rejects.toThrow(/must start at 6/);
  });

  it('rejects a gap (8–9 after 1–5) — next range must start at 6', async () => {
    await expect(
      attempt(serviceWith([rule('WAGON', 1, 5)]), {
        minWagonCount: 8,
        maxWagonCount: 9,
      }),
    ).rejects.toThrow(/must start at 6/);
  });

  it('accepts the contiguous continuation (6–10 after 1–5)', async () => {
    await expect(
      attempt(serviceWith([rule('WAGON', 1, 5)]), {
        minWagonCount: 6,
        maxWagonCount: 10,
      }),
    ).resolves.toBeUndefined();
  });

  it('after deleting a middle rule, the next range must fill the lowest gap', async () => {
    // Chain was 1–5, 6–10, 11–20; 6–10 deleted → next must start at 6.
    const svc = serviceWith([rule('WAGON', 1, 5), rule('WAGON', 11, 20)]);
    await expect(
      attempt(svc, { minWagonCount: 21, maxWagonCount: 25 }),
    ).rejects.toThrow(/must start at 6/);
    await expect(
      attempt(svc, { minWagonCount: 6, maxWagonCount: 10 }),
    ).resolves.toBeUndefined();
  });

  it('rejects a gap-fill that overruns into the next rule (6–15 into 11–20)', async () => {
    const svc = serviceWith([rule('WAGON', 1, 5), rule('WAGON', 11, 20)]);
    await expect(
      attempt(svc, { minWagonCount: 6, maxWagonCount: 15 }),
    ).rejects.toThrow(/overlaps existing rule/);
  });

  it('imposes no upper ceiling on any type', async () => {
    await expect(
      attempt(serviceWith([]), { minWagonCount: 1, maxWagonCount: 5000 }),
    ).resolves.toBeUndefined();
    await expect(
      attempt(serviceWith([]), {
        type: 'CURRENCY',
        currency: 'USD',
        minWagonCount: 1,
        maxWagonCount: 5000,
      }),
    ).resolves.toBeUndefined();
    await expect(
      attempt(serviceWith([]), {
        type: 'CUSTOMS',
        minWagonCount: 1,
        maxWagonCount: 5000,
      }),
    ).resolves.toBeUndefined();
  });

  it('keeps extending the chain past the old caps', async () => {
    await expect(
      attempt(serviceWith([rule('WAGON', 1, 50)]), {
        minWagonCount: 51,
        maxWagonCount: 120,
      }),
    ).resolves.toBeUndefined();
    await expect(
      attempt(serviceWith([rule('CUSTOMS', 1, 15)]), {
        type: 'CUSTOMS',
        minWagonCount: 16,
        maxWagonCount: 99,
      }),
    ).resolves.toBeUndefined();
  });

  it('still rejects a min greater than the max', async () => {
    await expect(
      attempt(serviceWith([]), { minWagonCount: 9, maxWagonCount: 4 }),
    ).rejects.toThrow(BadRequestException);
  });

  it('tracks CURRENCY chains per currency — USD and ETB are independent', async () => {
    const svc = serviceWith([rule('CURRENCY', 1, 5, 'USD')]);
    // ETB has no rules yet → starts at 1.
    await expect(
      attempt(svc, {
        type: 'CURRENCY',
        currency: 'ETB',
        minWagonCount: 1,
        maxWagonCount: 5,
      }),
    ).resolves.toBeUndefined();
    // USD must continue at 6.
    await expect(
      attempt(svc, {
        type: 'CURRENCY',
        currency: 'USD',
        minWagonCount: 1,
        maxWagonCount: 5,
      }),
    ).rejects.toThrow(/must start at 6/);
  });

  it('excludes the rule being edited from its own contiguity check', async () => {
    const existing = rule('WAGON', 6, 10, null, 'editing-me');
    const svc = serviceWith([rule('WAGON', 1, 5), existing]);
    // Re-saving 6–10 (e.g. changing points) keeps min 6 — allowed.
    await expect(
      attempt(svc, {
        minWagonCount: 6,
        maxWagonCount: 12,
        excludeId: 'editing-me',
      }),
    ).resolves.toBeUndefined();
  });

  it('lets an upper rule keep its start while a lower gap exists', async () => {
    // Chain 1–5, [gap 6–10], 11–20: editing 11–20 keeps min 11 — a lower gap
    // must not block editing an upper rule's points or max.
    const upper = rule('WAGON', 11, 20, null, 'upper');
    const svc = serviceWith([rule('WAGON', 1, 5), upper]);
    await expect(
      attempt(svc, {
        minWagonCount: 11,
        maxWagonCount: 25,
        excludeId: 'upper',
      }),
    ).resolves.toBeUndefined();
    // But it cannot RELOCATE to an arbitrary start — only keep 11 or fill 6.
    await expect(
      attempt(svc, {
        minWagonCount: 30,
        maxWagonCount: 35,
        excludeId: 'upper',
      }),
    ).rejects.toThrow(/must start at 6/);
  });

  it('reports the next-range prefill for the form', async () => {
    const svc = serviceWith([rule('WAGON', 1, 5), rule('WAGON', 11, 20)]);
    await expect(svc.nextRange('WAGON')).resolves.toEqual({ nextMin: 6 });
    // Past the old CUSTOMS cap of 15 the chain simply continues.
    await expect(
      serviceWith([rule('CUSTOMS', 1, 15)]).nextRange('CUSTOMS'),
    ).resolves.toEqual({ nextMin: 16 });
    await expect(serviceWith([]).nextRange('CURRENCY', 'USD')).resolves.toEqual({
      nextMin: 1,
    });
  });
});
