import { describe, expect, it } from 'vitest';

import { TypeOrmPersistenceSchemaError } from '../errors/TypeOrmPersistenceSchemaError';
import {
    assertArray,
    assertBoolean,
    assertDate,
    assertEnumValue,
    assertInteger,
    assertIsoDate,
    assertNonEmptyString,
    assertNumber,
    assertOptionalBoolean,
    assertOptionalInteger,
    assertOptionalNumber,
    assertOptionalStringOrNull,
    assertRecord,
} from './TypeOrmSchemaGuards';

describe('TypeOrmSchemaGuards', () => {
  it('assertNonEmptyString accepts non-empty strings', () => {
    expect(() => assertNonEmptyString('Driver', 'name', ' Max ')).not.toThrow();
  });

  it('assertNonEmptyString throws TypeOrmPersistenceSchemaError with metadata', () => {
    try {
      assertNonEmptyString('Driver', 'name', '');
      throw new Error('expected-to-throw');
    } catch (error) {
      expect(error).toBeInstanceOf(TypeOrmPersistenceSchemaError);
      expect(error).toMatchObject({
        name: 'TypeOrmPersistenceSchemaError',
        entityName: 'Driver',
        fieldName: 'name',
        reason: 'empty_string',
      });
    }
  });

  it('assertIsoDate accepts ISO string with exact toISOString match', () => {
    const value = '2025-01-01T00:00:00.000Z';
    expect(() => assertIsoDate('SeasonSchedule', 'startDate', value)).not.toThrow();
  });

  it('assertIsoDate rejects non-ISO / non-normalized values', () => {
    try {
      assertIsoDate('SeasonSchedule', 'startDate', '2025-01-01');
      throw new Error('expected-to-throw');
    } catch (error) {
      expect(error).toBeInstanceOf(TypeOrmPersistenceSchemaError);
      expect(error).toMatchObject({
        entityName: 'SeasonSchedule',
        fieldName: 'startDate',
        reason: 'not_iso_date',
      });
    }
  });

  it('assertDate accepts valid Date', () => {
    expect(() => assertDate('Driver', 'joinedAt', new Date())).not.toThrow();
  });

  it('assertDate rejects invalid Date objects', () => {
    try {
      assertDate('Driver', 'joinedAt', new Date('bad-date'));
      throw new Error('expected-to-throw');
    } catch (error) {
      expect(error).toBeInstanceOf(TypeOrmPersistenceSchemaError);
      expect(error).toMatchObject({
        entityName: 'Driver',
        fieldName: 'joinedAt',
        reason: 'invalid_date',
      });
    }
  });

  it('assertEnumValue accepts allowed enum strings', () => {
    expect(() => assertEnumValue('Race', 'status', 'scheduled', ['scheduled', 'running'] as const)).not.toThrow();
  });

  it('assertEnumValue rejects disallowed enum strings', () => {
    try {
      assertEnumValue('Race', 'status', 'completed', ['scheduled', 'running'] as const);
      throw new Error('expected-to-throw');
    } catch (error) {
      expect(error).toBeInstanceOf(TypeOrmPersistenceSchemaError);
      expect(error).toMatchObject({
        entityName: 'Race',
        fieldName: 'status',
        reason: 'invalid_enum_value',
      });
    }
  });

  it('assertArray accepts arrays and rejects non-arrays', () => {
    expect(() => assertArray('LeagueScoringConfig', 'championships', [])).not.toThrow();

    try {
      assertArray('LeagueScoringConfig', 'championships', {});
      throw new Error('expected-to-throw');
    } catch (error) {
      expect(error).toBeInstanceOf(TypeOrmPersistenceSchemaError);
      expect(error).toMatchObject({
        entityName: 'LeagueScoringConfig',
        fieldName: 'championships',
        reason: 'not_array',
      });
    }
  });

  it('assertRecord accepts plain objects and rejects arrays/null', () => {
    expect(() => assertRecord('League', 'settings', { a: 1 })).not.toThrow();

    try {
      assertRecord('League', 'settings', null);
      throw new Error('expected-to-throw');
    } catch (error) {
      expect(error).toBeInstanceOf(TypeOrmPersistenceSchemaError);
      expect(error).toMatchObject({
        entityName: 'League',
        fieldName: 'settings',
        reason: 'not_object',
      });
    }
  });

  it('assertNumber/assertInteger/assertBoolean accept correct primitives and reject others', () => {
    expect(() => assertNumber('League', 'settings.maxDrivers', 12.5)).not.toThrow();
    expect(() => assertInteger('League', 'settings.maxDrivers', 12)).not.toThrow();
    expect(() => assertBoolean('League', 'settings.requireDefense', true)).not.toThrow();

    try {
      assertNumber('League', 'settings.maxDrivers', '12');
      throw new Error('expected-to-throw');
    } catch (error) {
      expect(error).toBeInstanceOf(TypeOrmPersistenceSchemaError);
      expect(error).toMatchObject({
        entityName: 'League',
        fieldName: 'settings.maxDrivers',
        reason: 'not_number',
      });
    }

    try {
      assertInteger('League', 'settings.maxDrivers', 12.5);
      throw new Error('expected-to-throw');
    } catch (error) {
      expect(error).toBeInstanceOf(TypeOrmPersistenceSchemaError);
      expect(error).toMatchObject({
        entityName: 'League',
        fieldName: 'settings.maxDrivers',
        reason: 'not_integer',
      });
    }

    try {
      assertBoolean('League', 'settings.requireDefense', 1);
      throw new Error('expected-to-throw');
    } catch (error) {
      expect(error).toBeInstanceOf(TypeOrmPersistenceSchemaError);
      expect(error).toMatchObject({
        entityName: 'League',
        fieldName: 'settings.requireDefense',
        reason: 'not_boolean',
      });
    }
  });

  it('assertOptionalNumber/assertOptionalInteger/assertOptionalBoolean accept null/undefined and validate values when present', () => {
    expect(() => assertOptionalNumber('League', 'settings.sessionDuration', null)).not.toThrow();
    expect(() => assertOptionalInteger('League', 'settings.maxDrivers', undefined)).not.toThrow();
    expect(() => assertOptionalBoolean('League', 'settings.requireDefense', null)).not.toThrow();

    try {
      assertOptionalInteger('League', 'settings.maxDrivers', 12.5);
      throw new Error('expected-to-throw');
    } catch (error) {
      expect(error).toBeInstanceOf(TypeOrmPersistenceSchemaError);
      expect(error).toMatchObject({
        entityName: 'League',
        fieldName: 'settings.maxDrivers',
        reason: 'not_integer',
      });
    }
  });

  it('assertOptionalStringOrNull accepts undefined/null/string and rejects others', () => {
    expect(() => assertOptionalStringOrNull('Driver', 'bio', undefined)).not.toThrow();
    expect(() => assertOptionalStringOrNull('Driver', 'bio', null)).not.toThrow();
    expect(() => assertOptionalStringOrNull('Driver', 'bio', 'Bio')).not.toThrow();

    try {
      assertOptionalStringOrNull('Driver', 'bio', 123);
      throw new Error('expected-to-throw');
    } catch (error) {
      expect(error).toBeInstanceOf(TypeOrmPersistenceSchemaError);
      expect(error).toMatchObject({
        entityName: 'Driver',
        fieldName: 'bio',
        reason: 'not_string',
      });
    }
  });
});