import {
  noop
} from '@aurelia/kernel';
import {
  ValueConverter,
  Aurelia,
} from '@aurelia/runtime-html';
import {
  assert,
  createFixture,
  TestContext
} from '@aurelia/testing';

describe(`3-runtime-html/repeat.contextual-props.spec.ts`, function () {

  interface ISimpleRepeatContextualPropsTestCase {
    title: string;
    repeatExpression?: string;
    textExpression?: string;
    only?: boolean;
    testWillThrow?: boolean;
    mutationWillThrow?: boolean;
    getItems?(): any[] | Map<any, any> | Set<any>;
    mutate(collection: any[] | Map<any, any> | Set<any>, comp: any): void;
    expectation?(collection: any[] | Map<any, any> | Set<any>, comp: any): string;
  }

  // todo: enable tests that create new collection via value converter
  const simpleRepeatPropsTestCases: ISimpleRepeatContextualPropsTestCase[] = [
    ...[
      {
        title: `Basic - no mutation`,
        mutate() {/* nothing */}
      },
      {
        title: `Basic - set to [null]`,
        mutate(comp: ITestViewModel) {
          comp.items = null;
        }
      },
      {
        title: `Basic - set to [undefined]`,
        mutate(comp: ITestViewModel) {
          comp.items = undefined;
        }
      },
      {
        title: `Basic - with reverse()`,
        mutate: (items: ITestModel[]) => items.reverse()
      },
      {
        title: `Basic - with sort()`,
        mutate: (items: ITestModel[]) => items.sort(sortDesc)
      },
      {
        title: `Basic - with push()`,
        mutate(items: any[]) {
          for (let i = 0; 5 > i; ++i) {
            items.push({ name: `item - ${i}`, value: i });
          }
        }
      },
      {
        title: `Basic - with splice()`,
        mutate(items: any[]) {
          // todo: fix fail tests when doing multiple consecutive splices
          // for (let i = 0; 5 > i; ++i) {
          //   // tslint:disable-next-line:insecure-random
          //   const index = Math.floor(Math.random() * items.length);
          //   items.splice(index, 0, { name: `item - ${items.length}`, value: items.length });
          // }
          const index = Math.floor(Math.random() * items.length);
          items.splice(index, 0, { name: `item - ${items.length}`, value: items.length });
        }
      },
      {
        title: `Basic - with pop()`,
        mutate(items: any[]) {
          items.pop();
        }
      },
      {
        title: `Basic - with shift()`,
        mutate(items: any[]) {
          items.shift();
        }
      },
      {
        title: `Basic - with unshift()`,
        mutate(items: any[]) {
          items.unshift({ name: `item - abcd`, value: 100 });
        }
      },
    ].reduce(
      (allArrayCases, arrayCaseConfig) => {
        return allArrayCases.concat([
          arrayCaseConfig,
          {
            ...arrayCaseConfig,
            title: `${arrayCaseConfig.title} - with [Identity] value converter`,
            repeatExpression: `item of items | identity`
          },
          // {
          //   ...arrayCaseConfig,
          //   title: `${arrayCaseConfig.title} - with [Clone] value converter`,
          //   repeatExpression: `item of items | clone`
          // }
        ]);
      },
      []
    ),
    ...[
      {
        title: `Map basic - no mutation`,
        repeatExpression: `entry of items`,
        textExpression: `[\${entry[1].name}] -- \${$index} -- \${$even}`,
        getItems: () => new Map(createItems(10).map((item) => [item.name, item])),
        mutate() {/*  */}
      },
      {
        title: `Map basic - set to [null]`,
        repeatExpression: `entry of items`,
        textExpression: `[\${entry[1].name}] -- \${$index} -- \${$even}`,
        getItems: () => new Map(createItems(10).map((item) => [item.name, item])),
        mutate(comp: ITestViewModel) {
          comp.items = null;
        }
      },
      {
        title: `Map basic - set to [undefined]`,
        repeatExpression: `entry of items`,
        textExpression: `[\${entry[1].name}] -- \${$index} -- \${$even}`,
        getItems: () => new Map(createItems(10).map((item) => [item.name, item])),
        mutate(comp: ITestViewModel) {
          comp.items = undefined;
        }
      },
      {
        title: `Map basic - with set()`,
        repeatExpression: `entry of items`,
        textExpression: `[\${entry[1].name}] -- \${$index} -- \${$even}`,
        getItems: () => new Map(createItems(10).map((item) => [item.name, item])),
        mutate(items: Map<string, ITestModel>) {
          for (let i = 10; 15 > i; ++i) {
            items.set(`item - ${i}`, { name: `item - ${i}`, value: i });
          }
        }
      },
      {
        title: `Map basic - with delete()`,
        repeatExpression: `entry of items`,
        textExpression: `[\${entry[1].name}] -- \${$index} -- \${$even}`,
        getItems: () => new Map(createItems(10).map((item) => [item.name, item])),
        mutate(items: Map<string, ITestModel>) {
          for (let i = 0; 5 > i; ++i) {
            items.delete(`item - ${i}`);
          }
        }
      },
      {
        title: `Map basic - with clear()`,
        repeatExpression: `entry of items`,
        textExpression: `[\${entry[1].name}] -- \${$index} -- \${$even}`,
        getItems: () => new Map(createItems(10).map((item) => [item.name, item])),
        mutate(items: Map<string, ITestModel>) {
          items.clear();
        }
      }
    ].reduce(
      (allMapCases, mapCaseConfig) => {
        return allMapCases.concat([
          mapCaseConfig,
          {
            ...mapCaseConfig,
            title: `${mapCaseConfig.title} - with [Identity] value converter`,
            repeatExpression: `${mapCaseConfig.repeatExpression} | identity`
          },
          // {
          //   ...mapCaseConfig,
          //   title: `${mapCaseConfig.title} - with [Clone] value converter`,
          //   repeatExpression: `${mapCaseConfig.repeatExpression} | clone`
          // },
        ]);
      },
      []
    ),
    ...[
      {
        title: `Set basic - no mutation`,
        repeatExpression: `item of items`,
        textExpression: `[\${item.name}] -- \${$index} -- \${$even}`,
        getItems: () => new Set(createItems(10)),
        mutate() {/*  */}
      },
      {
        title: `Set basic - set to [null]`,
        repeatExpression: `item of items`,
        textExpression: `[\${item.name}] -- \${$index} -- \${$even}`,
        getItems: () => new Set(createItems(10)),
        mutate(comp: ITestViewModel) {
          comp.items = null;
        }
      },
      {
        title: `Set basic - set to [undefined]`,
        repeatExpression: `item of items`,
        textExpression: `[\${item.name}] -- \${$index} -- \${$even}`,
        getItems: () => new Set(createItems(10)),
        mutate(comp: ITestViewModel) {
          comp.items = undefined;
        }
      },
      {
        title: `Set basic - with add()`,
        repeatExpression: `item of items`,
        textExpression: `[\${item.name}] -- \${$index} -- \${$even}`,
        getItems: () => new Set(createItems(10)),
        mutate(items: Set<ITestModel>) {
          for (let i = 0; 5 > i; ++i) {
            items.add({ name: `item - ${i + 10}`, value: i + 10 });
          }
        }
      },
      {
        title: `Set basic - with delete()`,
        repeatExpression: `item of items`,
        textExpression: `[\${item.name}] -- \${$index} -- \${$even}`,
        getItems: () => new Set(createItems(10)),
        mutate(items: Set<ITestModel>) {
          const firstFive: ITestModel[] = [];
          let i = 0;
          items.forEach((item) => {
            if (i++ < 6) {
              firstFive.push(item);
            }
          });
          firstFive.forEach(item => {
            items.delete(item);
          });
        }
      },
      {
        title: `Set basic - with clear()`,
        repeatExpression: `item of items`,
        textExpression: `[\${item.name}] -- \${$index} -- \${$even}`,
        getItems: () => new Set(createItems(10)),
        mutate(items: Set<ITestModel>) {
          items.clear();
        }
      },
    ].reduce(
      (allSetCases, setCaseConfig) => {
        return allSetCases.concat([
          setCaseConfig,
          {
            ...setCaseConfig,
            title: `${setCaseConfig.title} - with [Identity] value converter`,
            repeatExpression: `${setCaseConfig.repeatExpression} | identity`
          },
          // {
          //   ...setCaseConfig,
          //   title: `${setCaseConfig.title} - with [Clone] value converter`,
          //   repeatExpression: `${setCaseConfig.repeatExpression} | clone`
          // }
        ]);
      },
      []
    ),
    ...[
      {
        title: `Number basic - no mutation`,
        textExpression: `[number:\${item}] -- \${$index} -- \${$even}`,
        getItems: () => 10,
        mutate() {/*  */}
      },
      {
        title: `Number basic - set to [null]`,
        textExpression: `[number:\${item}] -- \${$index} -- \${$even}`,
        getItems: () => 10,
        mutate(items: any, comp: ITestViewModel) {
          comp.items = null;
        }
      },
      {
        title: `Number basic - set to [undefined]`,
        textExpression: `[number:\${item}] -- \${$index} -- \${$even}`,
        getItems: () => 10,
        mutate(items: any, comp: ITestViewModel) {
          comp.items = undefined;
        }
      },
      {
        title: `Number basic - set to [0]`,
        textExpression: `[number:\${item}] -- \${$index} -- \${$even}`,
        getItems: () => 10,
        mutate(items: any, comp: ITestViewModel) {
          comp.items = 0;
        }
      },
      {
        title: `Number basic - set to [-10]`,
        textExpression: `[number:\${item}] -- \${$index} -- \${$even}`,
        mutationWillThrow: true,
        getItems: () => 10,
        mutate(items: any, comp: ITestViewModel) {
          comp.items = -10;
        }
      }
    ].reduce(
      (allNumberCases, numberCaseConfig) => {
        return allNumberCases.concat([
          numberCaseConfig,
          {
            ...numberCaseConfig,
            title: `${numberCaseConfig.title} - with [Identity] value converter`,
            repeatExpression: `item of items | identity`
          },
          // {
          //   ...numberCaseConfig,
          //   title: `${numberCaseConfig.title} - with [clone] value converter`,
          //   repeatExpression: `item of items | clone`
          // }
        ]);
      },
      []
    )
  ];

  // Some tests are using, some aren't
  // but always register these
  const IdentityValueConverter = ValueConverter.define(`identity`, class {
    public toView(val: any): any {
      return val;
    }
  });
  const CloneValueConverter = ValueConverter.define(`clone`, class {
    public toView(val: any): any {
      return Array.isArray(val)
        ? val.slice(0)
        : val instanceof Map
          ? new Map(val)
          : val instanceof Set
            ? new Set(val)
            : val;
    }
  });

  for (const testCase of simpleRepeatPropsTestCases) {
    const {
      title,
      getItems = () => createItems(10),
      repeatExpression = `item of items`,
      textExpression = `[\${item.name}] -- \${$index} -- \${$even}`,
      only,
      mutate = noop,
      expectation = defaultExpectation,
      testWillThrow,
      mutationWillThrow
    } = testCase;
    const template = `<div repeat.for="${repeatExpression}">${textExpression}</div>`;
    class Root {
      public items = getItems();
    }
    const suit = (_title: string, fn: any) => only
      // eslint-disable-next-line mocha/no-exclusive-tests
      ? it.only(_title, fn)
      : it(_title, fn);

    suit(title, function (): Promise<void> {
      // const ctx = TestContext.create();

      let au: Aurelia;
      let component: Root;
      let ctx: TestContext;
      // let body: HTMLElement;
      let host: HTMLElement;

      try {
        // au.app({ host, component: App });
        // await au.start();
        ({ component, au, ctx, appHost: host } = createFixture(template, Root, [IdentityValueConverter, CloneValueConverter]));
        assert.strictEqual(host.textContent, expectation(component.items, component), `#before mutation`);
      } catch (ex) {
        if (testWillThrow) {
          // dont try to assert anything on throw
          // just bails
          try {
            void au.stop();
          } catch {/* and ignore all errors trying to stop */}
          return;
        }
        throw ex;
      }

      if (testWillThrow) {
        throw new Error(`Expected test to throw, but did NOT`);
      }

      try {
        mutate(component.items, component);
        ctx.platform.domQueue.flush();

        assert.strictEqual(host.textContent, expectation(component.items, component), `#after mutation`);

        void au.stop();
      } catch (ex) {
        if (!mutationWillThrow) {
          try {
            void au.stop();
          } catch {
            /* and ignore all errors trying to stop */
          } finally {
            au.dispose();
          }
          throw ex;
        }
      }
    });
  }

  interface ITestViewModel {
    items: any;
  }

  interface ITestModel {
    name: string;
    value: number;
  }

  function createItems(count: number): ITestModel[] {
    return Array.from({ length: count }, (_, idx) => ({ name: `item - ${idx}`, value: idx }));
  }

  function defaultExpectation(items: any[] | Map<any, any> | Set<any>): string {
    if (Array.isArray(items)) {
      return items.map((item, idx) => `[${item.name}] -- ${idx} -- ${idx % 2 === 0}`).join(``);
    }
    if (items instanceof Map) {
      return Array
        .from(items.entries())
        .map(([itemName], idx) => `[${itemName}] -- ${idx} -- ${idx % 2 === 0}`)
        .join(``);
    }
    if (items instanceof Set) {
      return Array
        .from(items)
        .map((item: ITestModel, idx: number) => `[${item.name}] -- ${idx} -- ${idx % 2 === 0}`)
        .join(``);
    }
    if (items == null) {
      return ``;
    }
    if (typeof items === `number`) {
      let text = ``;
      for (let i = 0; items > i; ++i) {
        text += `[number:${i}] -- ${i} -- ${i % 2 === 0}`;
      }
      return text;
    }
    throw new Error(`Invalid item types`);
  }

  function sortDesc(item1: ITestModel, item2: ITestModel): -1 | 1 {
    return item1.value < item2.value ? 1 : -1;
  }
});
