/*
 * Copyright (C) 2018 StApps
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation, version 3.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program. If not, see <https://www.gnu.org/licenses/>.
 */
import {
  isLightweightClass,
  isLightweightEnum,
  isUnionType,
  LightweightAliasDefinition,
  LightweightProjectWithIndex,
  LightweightType,
  LightweightClassDefinition,
  LightweightDefinition,
  LightweightProperty,
} from '@openstapps/easy-ast';
import {expect} from 'chai';

process.on('unhandledRejection', error => {
  throw error;
});

describe('Features', () => {
  let project: LightweightProjectWithIndex;
  let thingNames: string[];
  let things: LightweightClassDefinition[];
  let thingsWithoutReferences: LightweightClassDefinition[];

  before(function () {
    this.timeout(15_000);
    this.slow(10_000);

    project = new LightweightProjectWithIndex('src');

    const thingsReflection = project.definitions['SCIndexableThings'] as LightweightAliasDefinition;
    expect(isLightweightEnum(thingsReflection)).to.be.true;
    expect(isUnionType(thingsReflection.type!)).to.be.true;

    thingsReflection.type!.specificationTypes!.push({
      flags: 524_288,
      referenceName: 'SCDiff',
    });

    expect(thingsReflection.type?.specificationTypes?.map(it => it.referenceName)).not.to.include(undefined);
    thingNames = thingsReflection.type?.specificationTypes?.map(type => type.referenceName!) ?? [];
    things = thingNames.map(it => project.definitions[it]).filter(isLightweightClass);
    thingsWithoutReferences = thingNames
      .map(it => project.definitions[`${it}WithoutReferences`])
      .filter(isLightweightClass);
  });

  const inheritedProperties = function (
    classLike: LightweightClassDefinition,
  ): Record<string, LightweightProperty> | undefined {
    const extendClause = [
      ...(classLike.implementedDefinitions ?? []),
      ...(classLike.extendedDefinitions ?? []),
    ];
    const properties = {...classLike.properties};

    for (const definition of extendClause) {
      const object = project.definitions[definition.referenceName!];
      if (isLightweightClass(object)) {
        Object.assign(properties, inheritedProperties(object));
      } else {
        Object.assign(properties, object);
      }
    }

    return properties;
  };

  it('should have an origin', () => {
    for (const thing of things) {
      expect(inheritedProperties(thing)?.['origin']).not.to.be.undefined;
    }
  });

  it('should not have duplicate names', () => {
    const names = new Set<string>();

    for (const fileName in project.files) {
      const file = project.files[fileName];
      for (const definition in file) {
        const definitionName = file[definition].name;
        expect(names).not.to.include(definitionName);
        names.add(definitionName);
      }
    }
  });

  it('should not have properties referencing SCThing', () => {
    const allPropertyReferenceNames: (property: LightweightProperty) => string[] = property =>
      [
        property.type.referenceName!,
        ...Object.values(property.properties ?? []).flatMap(allPropertyReferenceNames),
      ].filter(it => !!it);

    const typeHasSCThingReferences: (type?: LightweightType) => boolean = type =>
      type?.referenceName
        ? hasSCThingReferences(project.definitions[type.referenceName])
        : type?.specificationTypes?.some(typeHasSCThingReferences) === true;

    const hasSCThingReferences: (definition?: LightweightDefinition) => boolean = definition =>
      isLightweightClass(definition)
        ? Object.values(inheritedProperties(definition) ?? [])
            .flatMap(it => Object.values(it.properties ?? []).flatMap(allPropertyReferenceNames))
            .map(it => project.definitions[it] as LightweightDefinition)
            .some(it => it.name === 'SCThing' || hasSCThingReferences(it))
        : definition
          ? typeHasSCThingReferences(definition.type)
          : false;

    for (const thing of things) {
      expect(hasSCThingReferences(thing)).to.be.false;
    }
  });

  /**
   * Checks if a definition is an SCThing
   */
  function extendsSCThing(definition?: LightweightDefinition): boolean {
    return isLightweightClass(definition)
      ? [
          ...((definition as LightweightClassDefinition).extendedDefinitions ?? []),
          ...((definition as LightweightClassDefinition).implementedDefinitions ?? []),
        ]
          .map(it => it.referenceName)
          .filter(it => !!it)
          .some(it => it === 'SCThing' || extendsSCThing(project.definitions[it!]))
      : false;
  }

  it('should extend SCThing if it is an SCThing', () => {
    for (const thing of things) {
      expect(extendsSCThing(thing)).to.be.true;
    }
  });

  it('should not extend SCThing if it is an SCThingWithoutReferences', () => {
    for (const thingWithoutReferences of thingsWithoutReferences) {
      expect(extendsSCThing(thingWithoutReferences)).to.be.false;
    }
  });
});
