import { validate } from 'class-validator';
import { IsISO8601, IsOptional } from 'class-validator';

import { CLOCK_SKEW_TOLERANCE_MS, IsNotBackdated } from './is-not-backdated.validator';

class Subject {
  @IsOptional()
  @IsISO8601()
  @IsNotBackdated()
  occurredAt?: string;
}

const subjectWith = (occurredAt?: string) => {
  const subject = new Subject();
  subject.occurredAt = occurredAt;
  return subject;
};

const errorsFor = async (occurredAt?: string) => validate(subjectWith(occurredAt));

const backdatedErrors = (errors: Awaited<ReturnType<typeof errorsFor>>) =>
  errors.filter((error) => Object.keys(error.constraints ?? {}).includes('IsNotBackdated'));

describe('IsNotBackdated', () => {
  it('rejects a timestamp from the past', async () => {
    const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();

    const errors = await errorsFor(yesterday);

    expect(backdatedErrors(errors)).toHaveLength(1);
    expect(errors[0].constraints?.IsNotBackdated).toBe(
      'occurredAt cannot be backdated — it must be now or later',
    );
  });

  it('accepts now', async () => {
    const errors = await errorsFor(new Date().toISOString());

    expect(errors).toHaveLength(0);
  });

  it('accepts a value stale only by transit and clock skew', async () => {
    // What an honest caller sends: "now" as of when the request was built.
    const almostNow = new Date(Date.now() - (CLOCK_SKEW_TOLERANCE_MS - 5_000)).toISOString();

    const errors = await errorsFor(almostNow);

    expect(errors).toHaveLength(0);
  });

  it('rejects a value staler than the skew allowance', async () => {
    const tooStale = new Date(Date.now() - (CLOCK_SKEW_TOLERANCE_MS + 5_000)).toISOString();

    const errors = await errorsFor(tooStale);

    expect(backdatedErrors(errors)).toHaveLength(1);
  });

  it('ignores an absent value so @IsOptional decides', async () => {
    const errors = await errorsFor(undefined);

    expect(errors).toHaveLength(0);
  });

  it('leaves an unparseable value to the format validator', async () => {
    const errors = await errorsFor('not-a-date');

    // Reported as a format problem, not as a backdate.
    expect(backdatedErrors(errors)).toHaveLength(0);
    expect(errors[0].constraints).toHaveProperty('isIso8601');
  });
});
