import {
  reportTaskQueue,
  Task,
} from '@aurelia/platform';
import {
  DefaultLogEvent,
  DI,
  IContainer,
  ILogger,
  ISink,
  LoggerConfiguration,
  LogLevel,
  pascalCase,
  Registration,
  sink,
  optional,
  Class,
  resolve,
} from '@aurelia/kernel';
import {
  Scope,
} from '@aurelia/runtime';
import {
  type BindingBehaviorInstance,
  type IBinding,
  valueConverter,
  bindingBehavior,
  ValueConverter,
  Controller,
  customElement,
  CustomElement,
  Switch,
  Aurelia,
  IPlatform,
  ICustomElementViewModel,
  PromiseTemplateController,
  If,
  ISyntheticView,
  ICustomElementController,
  bindable,
  INode,
} from '@aurelia/runtime-html';
import {
  assert,
  TestContext,
} from '@aurelia/testing';
import {
  createSpecFunction,
  TestExecutionContext,
  TestFunction,
} from '../util.js';

describe('3-runtime-html/promise.spec.ts', function () {

  const phost = 'pending-host';
  const fhost = 'fulfilled-host';
  const rhost = 'rejected-host';
  type PromiseWithId<TValue = unknown> = Promise<TValue> & { id?: number };
  class Config {
    public constructor(
      public hasPromise: boolean,
      public wait: (name: string) => Promise<void> | void,
    ) { }

    public toString(): string {
      return `{${this.hasPromise ? this.wait.toString() : 'noWait'}}`;
    }
  }

  const configLookup = DI.createInterface<Map<string, Config>>();
  function createComponentType(name: string, template: string = '', bindables: string[] = []) {
    @customElement({ name, template, bindables })
    class Component {
      private logger: ILogger;
      private readonly $logger: ILogger;
      @bindable
      private readonly ceId: unknown = null;
      private readonly config: Config = resolve(optional(Config));
      public constructor() {
        const $logger = this.$logger = resolve(ILogger);
        const container = resolve(IContainer);
        const node = resolve(INode);
        if ((node as HTMLElement).dataset.logCtor !== void 0) {
          (this.logger = $logger.scopeTo(name)).debug('ctor');
          delete (node as HTMLElement).dataset.logCtor;
        }
        if (this.config == null) {
          const lookup = container.get(configLookup);
          this.config = lookup.get(name);
        }
      }

      public async binding(): Promise<void> {
        this.logger = this.ceId === null ? this.$logger.scopeTo(name) : this.$logger.scopeTo(`${name}-${this.ceId}`);
        if (this.config.hasPromise) {
          await this.config.wait('binding');
        }

        this.logger.debug('binding');
      }

      public async bound(): Promise<void> {
        if (this.config.hasPromise) {
          await this.config.wait('bound');
        }

        this.logger.debug('bound');
      }

      public async attaching(): Promise<void> {
        if (this.config.hasPromise) {
          await this.config.wait('attaching');
        }

        this.logger.debug('attaching');
      }

      public async attached(): Promise<void> {
        if (this.config.hasPromise) {
          await this.config.wait('attached');
        }

        this.logger.debug('attached');
      }

      public async detaching(): Promise<void> {
        if (this.config.hasPromise) {
          await this.config.wait('detaching');
        }

        this.logger.debug('detaching');
      }

      public async unbinding(): Promise<void> {
        if (this.config.hasPromise) {
          await this.config.wait('unbinding');
        }

        this.logger.debug('unbinding');
      }
    }

    Reflect.defineProperty(Component, 'name', {
      writable: false,
      enumerable: false,
      configurable: true,
      value: pascalCase(name),
    });

    return Component;
  }

  @sink({ handles: [LogLevel.debug] })
  class DebugLog implements ISink {
    public readonly log: string[] = [];
    public handleEvent(event: DefaultLogEvent): void {
      const scope = event.scope.join('.');
      if (scope.includes('-host')) {
        this.log.push(`${scope}.${event.message}`);
      }
    }
    public clear() {
      this.log.length = 0;
    }
  }

  interface TestSetupContext<TApp> {
    template: string;
    registrations: any[];
    expectedStopLog: string[];
    verifyStopCallsAsSet: boolean;
    promise: Promise<unknown> | (() => Promise<unknown>) | null;
    delayPromise: DelayPromise | null;
    appType: Class<TApp>;
  }
  class PromiseTestExecutionContext<TApp = App> implements TestExecutionContext<TApp> {
    private _scheduler: IPlatform;
    private readonly _log: DebugLog;
    public constructor(
      public ctx: TestContext,
      public container: IContainer,
      public host: HTMLElement,
      public app: TApp,
      public controller: Controller,
      public error: Error | null,
    ) {
      this._log = container.get(ILogger).sinks.find((s) => s instanceof DebugLog) as DebugLog;
    }
    public get platform(): IPlatform { return this._scheduler ?? (this._scheduler = this.container.get(IPlatform)); }
    public get log() {
      return this._log?.log ?? [];
    }
    public clear() {
      this._log?.clear();
    }

    public assertCalls(expected: string[], message: string = '') {
      assert.deepStrictEqual(this.log, expected, message);
    }

    public assertCallSet(expected: string[], message: string = '') {
      const actual = this.log;
      assert.strictEqual(actual.length, expected.length, `${message} - calls.length - ${actual}`);
      assert.strictEqual(actual.filter((c) => !expected.includes(c)).length, 0, `${message} - calls set equality -\n actual: \t ${actual}\n expected: \t ${expected}\n`);
    }
  }

  enum DelayPromise {
    binding = 'binding',
  }

  const seedPromise = DI.createInterface<Promise<unknown>>();
  const delaySeedPromise = DI.createInterface<DelayPromise>();
  async function testPromise<TApp extends object>(
    testFunction: TestFunction<PromiseTestExecutionContext<TApp>>,
    {
      template,
      registrations = [],
      expectedStopLog,
      verifyStopCallsAsSet = false,
      promise,
      delayPromise = null,
      appType,
    }: Partial<TestSetupContext<TApp>> = {}
  ) {
    const ctx = TestContext.create();

    const host = ctx.doc.createElement('div');
    ctx.doc.body.appendChild(host);

    const container = ctx.container;

    const au = new Aurelia(container);
    let error: Error | null = null;
    let app: TApp | null = null;
    let controller: Controller = null!;
    try {
      await au
        .register(
          LoggerConfiguration.create({ level: LogLevel.trace, sinks: [DebugLog] }),
          ...registrations,
          Promisify,
          Double,
          NoopBindingBehavior,
          typeof promise === 'function'
            ? Registration.callback(seedPromise, promise)
            : Registration.instance(seedPromise, promise),
          Registration.instance(delaySeedPromise, delayPromise),
        )
        .app({
          host,
          component: CustomElement.define({ name: 'app', template }, appType ?? App)
        })
        .start();
      app = au.root.controller.viewModel as TApp;
      controller = au.root.controller! as unknown as Controller;
    } catch (e) {
      error = e;
    }

    const testCtx = new PromiseTestExecutionContext(ctx, container, host, app, controller, error);
    await testFunction(testCtx);

    if (error === null) {
      testCtx.clear();
      await au.stop();
      assert.html.innerEqual(host, '', 'post-detach innerHTML');
      if (verifyStopCallsAsSet) {
        testCtx.assertCallSet(expectedStopLog);
      } else {
        testCtx.assertCalls(expectedStopLog, 'stop lifecycle calls');
      }
    }
    ctx.doc.body.removeChild(host);
  }
  const $it = createSpecFunction(testPromise);

  @valueConverter('promisify')
  class Promisify {
    public toView(value: unknown, resolve: boolean = true, ticks: number = 0): Promise<unknown> {
      if (ticks === 0) {
        return Object.assign(resolve ? Promise.resolve(value) : Promise.reject(new Error(String(value))), { id: 0 });
      }

      return Object.assign(
        createMultiTickPromise(ticks, () => resolve ? Promise.resolve(value) : Promise.reject(new Error(String(value))))(),
        { id: 0 }
      );
    }
  }

  @valueConverter('double')
  class Double {
    public fromView(value: unknown): unknown {
      return value instanceof Error
        ? (value.message = `${value.message} ${value.message}`, value)
        : `${value} ${value}`;
    }
  }

  @bindingBehavior('noop')
  class NoopBindingBehavior implements BindingBehaviorInstance {
    public bind(_scope: Scope, _binding: IBinding): void {
      return;
    }
    public unbind(_scope: Scope, _binding: IBinding): void {
      return;
    }
  }

  class App {
    public promise: PromiseWithId;
    private readonly container: IContainer = resolve(IContainer);
    private readonly delaySeedPromise: DelayPromise = resolve(delaySeedPromise);
    public constructor() {
      if (this.delaySeedPromise === null) {
        this.init();
      }
    }

    public binding(): void {
      if (this.delaySeedPromise !== DelayPromise.binding) { return; }
      this.init();
    }

    private init() {
      this.promise = this.container.get(seedPromise);
    }

    private updateError(err: Error) {
      err.message += '1';
      return err;
    }
  }

  function getActivationSequenceFor(name: string | string[], withCtor: boolean = false) {
    return typeof name === 'string'
      ? [...(withCtor ? [`${name}.ctor`] : []), `${name}.binding`, `${name}.bound`, `${name}.attaching`, `${name}.attached`]
      : [...(withCtor ? [`${name}.ctor`] : []), 'binding', 'bound', 'attaching', 'attached'].flatMap(x => name.map(n => `${n}.${x}`));
  }
  function getDeactivationSequenceFor(name: string | string[]) {
    return typeof name === 'string'
      ? [`${name}.detaching`, `${name}.unbinding`]
      : ['detaching', 'unbinding'].flatMap(x => name.map(n => `${n}.${x}`));
  }

  class TestData<TApp = App> implements TestSetupContext<TApp> {
    public readonly template: string;
    public readonly registrations: any[];
    public readonly verifyStopCallsAsSet: boolean;
    public readonly name: string;
    public readonly delayPromise: DelayPromise | null;
    public readonly appType: Class<TApp>;
    public constructor(
      name: string,
      public promise: Promise<unknown> | (() => Promise<unknown>) | null,
      {
        registrations = [],
        template,
        verifyStopCallsAsSet = false,
        delayPromise = null,
        appType,
      }: Partial<TestSetupContext<TApp>>,
      public readonly config: Config,
      public readonly expectedInnerHtml: string,
      public readonly expectedStartLog: string[],
      public readonly expectedStopLog: string[],
      public readonly additionalAssertions: ((ctx: PromiseTestExecutionContext<TApp>) => Promise<void> | void) | null = null,
      public readonly only: boolean = false,
    ) {
      this.name = `${name} - config: ${String(config)} - delayPromise: ${delayPromise}`;
      this.registrations = [
        ...(config !== null ? [Registration.instance(Config, config)] : []),
        createComponentType(phost, `pending\${p.id}`, ['p']),
        createComponentType(fhost, `resolved with \${data}`, ['data']),
        createComponentType(rhost, `rejected with \${err.message}`, ['err']),
        ...registrations,
      ];
      this.template = template;
      this.verifyStopCallsAsSet = verifyStopCallsAsSet;
      this.delayPromise = delayPromise;
      this.appType = appType as unknown as Class<TApp>;
    }
  }

  function createWaiter(ms: number): (name: string) => Promise<void> {
    function wait(_name: string): Promise<void> {
      return new Promise(function (resolve) { setTimeout(resolve, ms); });
    }

    wait.toString = function () {
      return `setTimeout(cb,${JSON.stringify(ms)})`;
    };

    return wait;
  }
  function noop(): Promise<void> {
    return;
  }

  noop.toString = function () {
    return 'Promise.resolve()';
  };

  function createMultiTickPromise(ticks: number, cb: () => Promise<unknown>) {
    const wait = () => {
      if (ticks === 0) {
        return cb();
      }
      ticks--;
      return new Promise((r) => setTimeout(function () { r(wait()); }, 0));
    };
    return wait;
  }

  function createWaiterWithTicks(ticks: Record<string, number>): (name?: string) => Promise<void> | void {
    const lookup: Record<string, () => Promise<void> | void> = Object.create(null);
    for (const [key, numTicks] of Object.entries(ticks)) {
      const fn: (() => Promise<void> | void) & { ticks: number } = () => {
        if (fn.ticks === 0) {
          return;
        }
        fn.ticks--;
        return new Promise((r) => setTimeout(function () { void r(fn()); }, 0));
      };
      fn.ticks = numTicks;
      lookup[key] = fn;
    }
    const wait = (name: string) => {
      return lookup[name]?.() ?? Promise.resolve();
    };
    wait.toString = function () {
      return `waitWithTicks(cb,${JSON.stringify(ticks)})`;
    };

    return wait;
  }

  function *getTestData() {
    function wrap(content: string, type: 'p' | 'f' | 'r', debugMode = false) {
      switch (type) {
        case 'p':
          return `<${phost}${debugMode ? ` p.bind="promise"` : ''}>${content}</${phost}>`;
        case 'f':
          return `<${fhost}${debugMode ? ` data.bind="data"` : ''}>${content}</${fhost}>`;
        case 'r':
          return `<${rhost}${debugMode ? ` err.bind="err"` : ''}>${content}</${rhost}>`;
      }
    }

    const configFactories = [
      function () {
        return new Config(false, noop);
      },
      function () {
        return new Config(true, createWaiter(0));
      },
      function () {
        return new Config(true, createWaiter(5));
      },
    ];
    for (const [pattribute, fattribute, rattribute] of [
      ['promise.bind', 'then.from-view', 'catch.from-view'],
      // TODO: activate after the attribute parser and/or interpreter such that for `t`, `then` is not picked up.
      ['promise.resolve', 'then', 'catch']
    ]) {
      const templateDiv = `
      <div ${pattribute}="promise">
        <pending-host pending p.bind="promise"></pending-host>
        <fulfilled-host ${fattribute}="data" data.bind="data"></fulfilled-host>
        <rejected-host ${rattribute}="err" err.bind="err"></rejected-host>
      </div>`;
      const template1 = `
    <template>
      <template ${pattribute}="promise">
        <pending-host pending p.bind="promise"></pending-host>
        <fulfilled-host ${fattribute}="data" data.bind="data"></fulfilled-host>
        <rejected-host ${rattribute}="err" err.bind="err"></rejected-host>
      </template>
    </template>`;
      for (const delayPromise of [null, ...(Object.values(DelayPromise))]) {
        for (const config of configFactories) {
          {
            let resolve: (value: unknown) => void;
            yield new TestData(
              'shows content as per promise status - non-template promise-host - fulfilled',
              Object.assign(new Promise((r) => resolve = r), { id: 0 }),
              { delayPromise, template: templateDiv, },
              config(),
              `<div> ${wrap('pending0', 'p')} </div>`,
              getActivationSequenceFor(phost),
              getDeactivationSequenceFor(fhost),
              async (ctx) => {
                ctx.clear();
                resolve(42);
                const p = ctx.platform;
                // one tick to call back the fulfill delegate, and queue task
                await p.domQueue.yield();
                // on the next tick wait the queued task
                await p.domQueue.yield();
                assert.html.innerEqual(ctx.host, `<div> ${wrap('resolved with 42', 'f')} </div>`);
                ctx.assertCallSet([...getDeactivationSequenceFor(phost), ...getActivationSequenceFor(fhost)]);
              }
            );
          }
          {
            let reject: (value: unknown) => void;
            yield new TestData(
              'shows content as per promise status #1 - rejected',
              Object.assign(new Promise((_, r) => reject = r), { id: 0 }),
              { delayPromise, template: templateDiv },
              config(),
              `<div> ${wrap('pending0', 'p')} </div>`,
              getActivationSequenceFor(phost),
              getDeactivationSequenceFor(rhost),
              async (ctx) => {
                ctx.clear();
                reject(new Error('foo-bar'));
                const p = ctx.platform;
                await p.domQueue.yield();
                await p.domQueue.yield();
                assert.html.innerEqual(ctx.host, `<div> ${wrap('rejected with foo-bar', 'r')} </div>`);
                ctx.assertCallSet([...getDeactivationSequenceFor(phost), ...getActivationSequenceFor(rhost)]);
              }
            );
          }
          {
            let resolve: (value: unknown) => void;
            yield new TestData(
              'shows content as per promise status #1 - fulfilled',
              Object.assign(new Promise((r) => resolve = r), { id: 0 }),
              { delayPromise, template: template1, },
              config(),
              wrap('pending0', 'p'),
              getActivationSequenceFor(phost),
              getDeactivationSequenceFor(fhost),
              async (ctx) => {
                ctx.clear();
                resolve(42);
                const p = ctx.platform;
                // one tick to call back the fulfill delegate, and queue task
                await p.domQueue.yield();
                // on the next tick wait the queued task
                await p.domQueue.yield();
                assert.html.innerEqual(ctx.host, wrap('resolved with 42', 'f'));
                ctx.assertCallSet([...getDeactivationSequenceFor(phost), ...getActivationSequenceFor(fhost)]);
              },
            );
          }
          {
            let reject: (value: unknown) => void;
            yield new TestData(
              'shows content as per promise status #1 - rejected',
              Object.assign(new Promise((_, r) => reject = r), { id: 0 }),
              { delayPromise, template: template1 },
              config(),
              wrap('pending0', 'p'),
              getActivationSequenceFor(phost),
              getDeactivationSequenceFor(rhost),
              async (ctx) => {
                ctx.clear();
                reject(new Error('foo-bar'));
                const p = ctx.platform;
                await p.domQueue.yield();
                await p.domQueue.yield();
                assert.html.innerEqual(ctx.host, wrap('rejected with foo-bar', 'r'));
                ctx.assertCallSet([...getDeactivationSequenceFor(phost), ...getActivationSequenceFor(rhost)]);
              }
            );
          }
          yield new TestData(
            'shows content for resolved promise',
            Promise.resolve(42),
            { delayPromise, template: template1 },
            config(),
            wrap('resolved with 42', 'f'),
            getActivationSequenceFor(fhost),
            getDeactivationSequenceFor(fhost),
          );
          yield new TestData(
            'shows content for rejected promise',
            () => Promise.reject(new Error('foo-bar')),
            { delayPromise, template: template1 },
            config(),
            wrap('rejected with foo-bar', 'r'),
            getActivationSequenceFor(rhost),
            getDeactivationSequenceFor(rhost),
          );
          yield new TestData(
            'reacts to change in promise value - fulfilled -> fulfilled',
            Promise.resolve(42),
            { delayPromise, template: template1 },
            config(),
            wrap('resolved with 42', 'f'),
            getActivationSequenceFor(fhost),
            getDeactivationSequenceFor(fhost),
            async (ctx) => {
              ctx.clear();
              const p = ctx.platform;
              ctx.app.promise = Promise.resolve(24);
              await p.domQueue.yield();
              assert.html.innerEqual(ctx.host, wrap('resolved with 24', 'f'));
              ctx.assertCallSet([]);
            }
          );
          yield new TestData(
            'reacts to change in promise value - fulfilled -> rejected',
            Promise.resolve(42),
            { delayPromise, template: template1 },
            config(),
            wrap('resolved with 42', 'f'),
            getActivationSequenceFor(fhost),
            getDeactivationSequenceFor(rhost),
            async (ctx) => {
              ctx.clear();
              const p = ctx.platform;
              ctx.app.promise = Promise.reject(new Error('foo-bar'));
              await p.domQueue.yield();
              assert.html.innerEqual(ctx.host, wrap('rejected with foo-bar', 'r'));
              ctx.assertCallSet([...getDeactivationSequenceFor(fhost), ...getActivationSequenceFor(rhost)]);
            }
          );
          yield new TestData(
            'reacts to change in promise value - fulfilled -> (pending -> fulfilled) + deferred view-instantiation assertion',
            Promise.resolve(42),
            {
              delayPromise, template: `
            <template>
              <template ${pattribute}="promise">
                <pending-host pending p.bind="promise" data-log-ctor></pending-host>
                <fulfilled-host ${fattribute}="data" data.bind="data" data-log-ctor></fulfilled-host>
                <rejected-host ${rattribute}="err" err.bind="err" data-log-ctor></rejected-host>
              </template>
            </template>` },
            config(),
            wrap('resolved with 42', 'f'),
            getActivationSequenceFor(fhost, true),
            getDeactivationSequenceFor(fhost),
            async (ctx) => {
              ctx.clear();
              const p = ctx.platform;
              let resolve: (value: unknown) => void;
              const promise: PromiseWithId = new Promise((r) => resolve = r);
              promise.id = 0;
              ctx.app.promise = promise;
              await p.domQueue.yield();

              assert.html.innerEqual(ctx.host, wrap('pending0', 'p'));
              ctx.assertCallSet([...getDeactivationSequenceFor(fhost), ...getActivationSequenceFor(phost, true)]);
              ctx.clear();

              resolve(84);
              await p.domQueue.yield();
              await p.domQueue.yield();
              assert.html.innerEqual(ctx.host, wrap('resolved with 84', 'f'));
              ctx.assertCallSet([...getDeactivationSequenceFor(phost), ...getActivationSequenceFor(fhost)]);
            }
          );
          yield new TestData(
            'reacts to change in promise value - fulfilled -> (pending -> rejected)',
            Promise.resolve(42),
            { delayPromise, template: template1 },
            config(),
            wrap('resolved with 42', 'f'),
            getActivationSequenceFor(fhost),
            getDeactivationSequenceFor(rhost),
            async (ctx) => {
              ctx.clear();
              const p = ctx.platform;
              let reject: (value: unknown) => void;
              const promise: PromiseWithId = new Promise((_, r) => reject = r);
              promise.id = 0;
              ctx.app.promise = promise;
              await p.domQueue.yield();

              assert.html.innerEqual(ctx.host, wrap('pending0', 'p'));
              ctx.assertCallSet([...getDeactivationSequenceFor(fhost), ...getActivationSequenceFor(phost)]);
              ctx.clear();

              reject(new Error('foo-bar'));
              await p.domQueue.yield();
              await p.domQueue.yield();
              assert.html.innerEqual(ctx.host, wrap('rejected with foo-bar', 'r'));
              ctx.assertCallSet([...getDeactivationSequenceFor(phost), ...getActivationSequenceFor(rhost)]);
            }
          );
          yield new TestData(
            'reacts to change in promise value - rejected -> rejected',
            () => Promise.reject(new Error('foo-bar')),
            { delayPromise, template: template1 },
            config(),
            wrap('rejected with foo-bar', 'r'),
            getActivationSequenceFor(rhost),
            getDeactivationSequenceFor(rhost),
            async (ctx) => {
              ctx.clear();
              const p = ctx.platform;
              ctx.app.promise = Promise.reject(new Error('fizz-bazz'));
              await p.domQueue.yield();
              assert.html.innerEqual(ctx.host, wrap('rejected with fizz-bazz', 'r'));
              ctx.assertCallSet([]);
            }
          );
          yield new TestData(
            'reacts to change in promise value - rejected -> fulfilled',
            () => Promise.reject(new Error('foo-bar')),
            { delayPromise, template: template1 },
            config(),
            wrap('rejected with foo-bar', 'r'),
            getActivationSequenceFor(rhost),
            getDeactivationSequenceFor(fhost),
            async (ctx) => {
              ctx.clear();
              const p = ctx.platform;
              ctx.app.promise = Promise.resolve(42);
              await p.domQueue.yield();
              assert.html.innerEqual(ctx.host, wrap('resolved with 42', 'f'));
              ctx.assertCallSet([...getDeactivationSequenceFor(rhost), ...getActivationSequenceFor(fhost)]);
            }
          );
          yield new TestData(
            'reacts to change in promise value - rejected -> (pending -> fulfilled)',
            () => Promise.reject(new Error('foo-bar')),
            { delayPromise, template: template1 },
            config(),
            wrap('rejected with foo-bar', 'r'),
            getActivationSequenceFor(rhost),
            getDeactivationSequenceFor(fhost),
            async (ctx) => {
              ctx.clear();
              const p = ctx.platform;
              let resolve: (value: unknown) => void;
              const promise: PromiseWithId = new Promise((r) => resolve = r);
              promise.id = 0;
              ctx.app.promise = promise;
              await p.domQueue.yield();

              assert.html.innerEqual(ctx.host, wrap('pending0', 'p'));
              ctx.assertCallSet([...getDeactivationSequenceFor(rhost), ...getActivationSequenceFor(phost)]);
              ctx.clear();

              resolve(84);
              await p.domQueue.yield();
              await p.domQueue.yield();
              assert.html.innerEqual(ctx.host, wrap('resolved with 84', 'f'));
              ctx.assertCallSet([...getDeactivationSequenceFor(phost), ...getActivationSequenceFor(fhost)]);
            }
          );
          yield new TestData(
            'reacts to change in promise value - rejected -> (pending -> rejected)',
            () => Promise.reject(new Error('foo-bar')),
            { delayPromise, template: template1 },
            config(),
            wrap('rejected with foo-bar', 'r'),
            getActivationSequenceFor(rhost),
            getDeactivationSequenceFor(rhost),
            async (ctx) => {
              ctx.clear();
              const p = ctx.platform;
              let reject: (value: unknown) => void;
              const promise: PromiseWithId = new Promise((_, r) => reject = r);
              promise.id = 0;
              ctx.app.promise = promise;
              await p.domQueue.yield();

              assert.html.innerEqual(ctx.host, wrap('pending0', 'p'));
              ctx.assertCallSet([...getDeactivationSequenceFor(rhost), ...getActivationSequenceFor(phost)]);
              ctx.clear();

              reject(new Error('foo-bar'));
              await p.domQueue.yield();
              await p.domQueue.yield();
              assert.html.innerEqual(ctx.host, wrap('rejected with foo-bar', 'r'));
              ctx.assertCallSet([...getDeactivationSequenceFor(phost), ...getActivationSequenceFor(rhost)]);
            }
          );
          yield new TestData(
            'reacts to change in promise value - pending -> pending',
            Object.assign(new Promise(() => {/* noop */ }), { id: 0 }),
            { delayPromise, template: template1 },
            config(),
            wrap('pending0', 'p'),
            getActivationSequenceFor(phost),
            getDeactivationSequenceFor(phost),
            async (ctx) => {
              ctx.clear();
              const p = ctx.platform;
              ctx.app.promise = Object.assign(new Promise(() => {/* noop */ }), { id: 1 });
              await p.domQueue.yield();

              assert.html.innerEqual(ctx.host, wrap('pending1', 'p'));
              ctx.assertCallSet([]);
            }
          );
          yield new TestData(
            'reacts to change in promise value - pending -> fulfilled',
            Object.assign(new Promise(() => {/* noop */ }), { id: 0 }),
            { delayPromise, template: template1 },
            config(),
            wrap('pending0', 'p'),
            getActivationSequenceFor(phost),
            getDeactivationSequenceFor(fhost),
            async (ctx) => {
              ctx.clear();
              const p = ctx.platform;
              ctx.app.promise = Promise.resolve(42);
              await p.domQueue.yield();

              assert.html.innerEqual(ctx.host, wrap('resolved with 42', 'f'));
              ctx.assertCallSet([...getDeactivationSequenceFor(phost), ...getActivationSequenceFor(fhost)]);
            }
          );
          yield new TestData(
            'reacts to change in promise value - pending -> rejected',
            Object.assign(new Promise(() => {/* noop */ }), { id: 0 }),
            { delayPromise, template: template1 },
            config(),
            wrap('pending0', 'p'),
            getActivationSequenceFor(phost),
            getDeactivationSequenceFor(rhost),
            async (ctx) => {
              ctx.clear();
              const p = ctx.platform;
              ctx.app.promise = Promise.reject(new Error('foo-bar'));
              await p.domQueue.yield();

              assert.html.innerEqual(ctx.host, wrap('rejected with foo-bar', 'r'));
              ctx.assertCallSet([...getDeactivationSequenceFor(phost), ...getActivationSequenceFor(rhost)]);
            }
          );
          yield new TestData(
            'reacts to change in promise value - pending -> (pending -> fulfilled)',
            Object.assign(new Promise(() => {/* noop */ }), { id: 0 }),
            { delayPromise, template: template1 },
            config(),
            wrap('pending0', 'p'),
            getActivationSequenceFor(phost),
            getDeactivationSequenceFor(fhost),
            async (ctx) => {
              ctx.clear();
              const p = ctx.platform;
              let resolve: (value: unknown) => void;
              ctx.app.promise = Object.assign(new Promise((r) => resolve = r), { id: 1 });
              await p.domQueue.yield();

              assert.html.innerEqual(ctx.host, wrap('pending1', 'p'));
              ctx.assertCallSet([]);

              resolve(42);
              await p.domQueue.yield();
              await p.domQueue.yield();
              assert.html.innerEqual(ctx.host, wrap('resolved with 42', 'f'));
              ctx.assertCallSet([...getDeactivationSequenceFor(phost), ...getActivationSequenceFor(fhost)]);
            }
          );
          yield new TestData(
            'reacts to change in promise value - pending -> (pending -> rejected)',
            Object.assign(new Promise(() => {/* noop */ }), { id: 0 }),
            { delayPromise, template: template1 },
            config(),
            wrap('pending0', 'p'),
            getActivationSequenceFor(phost),
            getDeactivationSequenceFor(rhost),
            async (ctx) => {
              ctx.clear();
              const p = ctx.platform;
              let reject: (value: unknown) => void;
              ctx.app.promise = Object.assign(new Promise((_, r) => reject = r), { id: 1 });
              await p.domQueue.yield();

              assert.html.innerEqual(ctx.host, wrap('pending1', 'p'));
              ctx.assertCallSet([]);

              reject(new Error('foo-bar'));
              await p.domQueue.yield();
              await p.domQueue.yield();
              assert.html.innerEqual(ctx.host, wrap('rejected with foo-bar', 'r'));
              ctx.assertCallSet([...getDeactivationSequenceFor(phost), ...getActivationSequenceFor(rhost)]);
            }
          );
          yield new TestData(
            'can be used in isolation without any of the child template controllers',
            new Promise(() => {/* noop */ }),
            { delayPromise, template: `<template><template ${pattribute}="promise">this is shown always</template></template>` },
            config(),
            'this is shown always',
            [],
            [],
          );
          const pTemplt =
            `<template>
        <template ${pattribute}="promise">
          <pending-host pending p.bind="promise"></pending-host>
        </template>
      </template>`;
          {
            let resolve: (value: unknown) => void;
            yield new TestData(
              'supports combination: promise>pending - resolved',
              Object.assign(new Promise((r) => resolve = r), { id: 0 }),
              { delayPromise, template: pTemplt },
              config(),
              wrap('pending0', 'p'),
              getActivationSequenceFor(phost),
              [],
              async (ctx) => {
                ctx.clear();
                resolve(42);
                const q = ctx.platform.domQueue;
                await q.yield();
                await q.yield();
                assert.html.innerEqual(ctx.host, '');
                ctx.assertCallSet(getDeactivationSequenceFor(phost));
              }
            );
          }
          {
            let reject: (value: unknown) => void;
            yield new TestData(
              'supports combination: promise>pending - rejected',
              Object.assign(new Promise((_, r) => reject = r), { id: 0 }),
              { delayPromise, template: pTemplt },
              config(),
              wrap('pending0', 'p'),
              getActivationSequenceFor(phost),
              [],
              async (ctx) => {
                ctx.clear();
                reject(new Error('foo-bar'));
                const q = ctx.platform.domQueue;
                await q.yield();
                await q.yield();
                assert.html.innerEqual(ctx.host, '');
                ctx.assertCallSet(getDeactivationSequenceFor(phost));
              }
            );
          }
          const pfCombTemplt =
            `<template>
        <template ${pattribute}="promise">
          <pending-host pending p.bind="promise"></pending-host>
          <fulfilled-host ${fattribute}="data" data.bind="data"></fulfilled-host>
        </template>
      </template>`;
          {
            let resolve: (value: unknown) => void;
            yield new TestData(
              'supports combination: promise>(pending+then) - resolved',
              Object.assign(new Promise((r) => resolve = r), { id: 0 }),
              { delayPromise, template: pfCombTemplt },
              config(),
              wrap('pending0', 'p'),
              getActivationSequenceFor(phost),
              getDeactivationSequenceFor(fhost),
              async (ctx) => {
                ctx.clear();
                resolve(42);
                const q = ctx.platform.domQueue;
                await q.yield();
                await q.yield();
                assert.html.innerEqual(ctx.host, wrap('resolved with 42', 'f'));
                ctx.assertCallSet([...getDeactivationSequenceFor(phost), ...getActivationSequenceFor(fhost)]);
              }
            );
          }
          {
            let reject: (value: unknown) => void;
            yield new TestData(
              'supports combination: promise>(pending+then) - rejected',
              Object.assign(new Promise((_, r) => reject = r), { id: 0 }),
              { delayPromise, template: pfCombTemplt },
              config(),
              wrap('pending0', 'p'),
              getActivationSequenceFor(phost),
              [],
              async (ctx) => {
                ctx.clear();
                reject(new Error('foo-bar'));
                const q = ctx.platform.domQueue;
                await q.yield();
                await q.yield();
                assert.html.innerEqual(ctx.host, '');
                ctx.assertCallSet(getDeactivationSequenceFor(phost));
              }
            );
          }
          const prCombTemplt =
            `<template>
        <template ${pattribute}="promise">
          <pending-host pending p.bind="promise"></pending-host>
          <rejected-host ${rattribute}="err" err.bind="err"></rejected-host>
        </template>
      </template>`;
          {
            let resolve: (value: unknown) => void;
            yield new TestData(
              'supports combination: promise>(pending+catch) - resolved',
              Object.assign(new Promise((r) => resolve = r), { id: 0 }),
              { delayPromise, template: prCombTemplt },
              config(),
              wrap('pending0', 'p'),
              getActivationSequenceFor(phost),
              [],
              async (ctx) => {
                ctx.clear();
                resolve(42);
                const q = ctx.platform.domQueue;
                await q.yield();
                await q.yield();
                assert.html.innerEqual(ctx.host, '');
                ctx.assertCallSet(getDeactivationSequenceFor(phost));
              }
            );
          }
          {
            let reject: (value: unknown) => void;
            yield new TestData(
              'supports combination: promise>(pending+catch) - rejected',
              Object.assign(new Promise((_, r) => reject = r), { id: 0 }),
              { delayPromise, template: prCombTemplt },
              config(),
              wrap('pending0', 'p'),
              getActivationSequenceFor(phost),
              getDeactivationSequenceFor(rhost),
              async (ctx) => {
                ctx.clear();
                reject(new Error('foo-bar'));
                const q = ctx.platform.domQueue;
                await q.yield();
                await q.yield();
                assert.html.innerEqual(ctx.host, wrap('rejected with foo-bar', 'r'));
                ctx.assertCallSet([...getDeactivationSequenceFor(phost), ...getActivationSequenceFor(rhost)]);
              }
            );
          }
          const fTemplt =
            `<template>
      <template ${pattribute}="promise">
        <fulfilled-host ${fattribute}="data" data.bind="data"></fulfilled-host>
      </template>
    </template>`;
          {
            let resolve: (value: unknown) => void;
            yield new TestData(
              'supports combination: promise>then - resolved',
              Object.assign(new Promise((r) => resolve = r), { id: 0 }),
              { delayPromise, template: fTemplt },
              config(),
              '',
              [],
              getDeactivationSequenceFor(fhost),
              async (ctx) => {
                ctx.clear();
                resolve(42);
                const q = ctx.platform.domQueue;
                await q.yield();
                await q.yield();
                assert.html.innerEqual(ctx.host, wrap('resolved with 42', 'f'));
                ctx.assertCallSet(getActivationSequenceFor(fhost));
              }
            );
          }
          {
            let reject: (value: unknown) => void;
            yield new TestData(
              'supports combination: promise>then - rejected',
              Object.assign(new Promise((_, r) => reject = r), { id: 0 }),
              { delayPromise, template: fTemplt },
              config(),
              '',
              [],
              [],
              async (ctx) => {
                ctx.clear();
                reject(new Error('foo-bar'));
                const q = ctx.platform.domQueue;
                await q.yield();
                await q.yield();
                assert.html.innerEqual(ctx.host, '');
                ctx.assertCallSet([]);
              }
            );
          }
          const rTemplt =
            `<template>
      <template ${pattribute}="promise">
        <rejected-host ${rattribute}="err" err.bind="err"></rejected-host>
      </template>
    </template>`;
          {
            let resolve: (value: unknown) => void;
            yield new TestData(
              'supports combination: promise>catch - resolved',
              new Promise((r) => resolve = r),
              { delayPromise, template: rTemplt },
              config(),
              '',
              [],
              [],
              async (ctx) => {
                ctx.clear();
                resolve(42);
                const q = ctx.platform.domQueue;
                await q.yield();
                await q.yield();
                assert.html.innerEqual(ctx.host, '');
                ctx.assertCallSet([]);
              }
            );
          }
          {
            let reject: (value: unknown) => void;
            yield new TestData(
              'supports combination: promise>catch - rejected',
              new Promise((_, r) => reject = r),
              { delayPromise, template: rTemplt },
              config(),
              '',
              [],
              getDeactivationSequenceFor(rhost),
              async (ctx) => {
                ctx.clear();
                reject(new Error('foo-bar'));
                const q = ctx.platform.domQueue;
                await q.yield();
                await q.yield();
                assert.html.innerEqual(ctx.host, wrap('rejected with foo-bar', 'r'));
                ctx.assertCallSet(getActivationSequenceFor(rhost));
              }
            );
          }
          const frTemplt =
            `<template>
      <template ${pattribute}="promise">
        <fulfilled-host ${fattribute}="data" data.bind="data"></fulfilled-host>
        <rejected-host ${rattribute}="err" err.bind="err"></rejected-host>
      </template>
    </template>`;
          {
            let resolve: (value: unknown) => void;
            yield new TestData(
              'supports combination: promise>then+catch - resolved',
              new Promise((r) => resolve = r),
              { delayPromise, template: frTemplt },
              config(),
              '',
              [],
              getDeactivationSequenceFor(fhost),
              async (ctx) => {
                ctx.clear();
                resolve(42);
                const q = ctx.platform.domQueue;
                await q.yield();
                await q.yield();
                assert.html.innerEqual(ctx.host, wrap('resolved with 42', 'f'));
                ctx.assertCallSet(getActivationSequenceFor(fhost));
              }
            );
          }
          {
            let reject: (value: unknown) => void;
            yield new TestData(
              'supports combination: promise>then+catch - rejected',
              new Promise((_, r) => reject = r),
              { delayPromise, template: frTemplt },
              config(),
              '',
              [],
              getDeactivationSequenceFor(rhost),
              async (ctx) => {
                ctx.clear();
                reject(new Error('foo-bar'));
                const q = ctx.platform.domQueue;
                await q.yield();
                await q.yield();
                assert.html.innerEqual(ctx.host, wrap('rejected with foo-bar', 'r'));
                ctx.assertCallSet(getActivationSequenceFor(rhost));
              }
            );
          }

          yield new TestData(
            'shows static elements',
            Promise.resolve(42),
            {
              delayPromise, template: `
        <template>
          <template ${pattribute}="promise">
            <div>foo</div>
          </template>
        </template>` },
            config(),
            '<div>foo</div>',
            [],
            [],
          );

          const template2 = `
    <template>
      <template ${pattribute}="promise">
        <pending-host pending p.bind="promise"></pending-host>
        <fulfilled-host1 then></fulfilled-host1>
        <rejected-host1 catch></rejected-host1>
      </template>
    </template>`;
          {
            let resolve: (value: unknown) => void;
            yield new TestData(
              'shows content as per promise status #2 - fulfilled',
              Object.assign(new Promise((r) => resolve = r), { id: 0 }),
              {
                delayPromise, template: template2,
                registrations: [
                  createComponentType('fulfilled-host1', 'resolved'),
                  createComponentType('rejected-host1', 'rejected'),
                ]
              },
              config(),
              wrap('pending0', 'p'),
              getActivationSequenceFor(phost),
              getDeactivationSequenceFor('fulfilled-host1'),
              async (ctx) => {
                ctx.clear();
                resolve(42);
                const p = ctx.platform;
                // one tick to call back the fulfill delegate, and queue task
                await p.domQueue.yield();
                // on the next tick wait the queued task
                await p.domQueue.yield();
                assert.html.innerEqual(ctx.host, '<fulfilled-host1>resolved</fulfilled-host1>');
                ctx.assertCallSet([...getDeactivationSequenceFor(phost), ...getActivationSequenceFor('fulfilled-host1')]);
              }
            );
          }
          {
            let reject: (value: unknown) => void;
            yield new TestData(
              'shows content as per promise status #2 - rejected',
              Object.assign(new Promise((_, r) => reject = r), { id: 0 }),
              {
                delayPromise, template: template2,
                registrations: [
                  createComponentType('fulfilled-host1', 'resolved'),
                  createComponentType('rejected-host1', 'rejected'),
                ]
              },
              config(),
              wrap('pending0', 'p'),
              getActivationSequenceFor(phost),
              getDeactivationSequenceFor('rejected-host1'),
              async (ctx) => {
                ctx.clear();
                reject(new Error());
                const p = ctx.platform;
                // one tick to call back the fulfill delegate, and queue task
                await p.domQueue.yield();
                // on the next tick wait the queued task
                await p.domQueue.yield();
                assert.html.innerEqual(ctx.host, '<rejected-host1>rejected</rejected-host1>');
                ctx.assertCallSet([...getDeactivationSequenceFor(phost), ...getActivationSequenceFor('rejected-host1')]);
              }
            );
          }
          yield new TestData(
            'works in nested template - fulfilled>fulfilled',
            Promise.resolve({ json() { return Promise.resolve(42); } }),
            {
              delayPromise, template: `
            <template>
              <template ${pattribute}="promise">
                <pending-host pending p.bind="promise"></pending-host>
                <template ${fattribute}="response" ${pattribute}="response.json()">
                  <fulfilled-host ${fattribute}="data" data.bind="data"></fulfilled-host>
                </template>
                <rejected-host ${rattribute}="err" err.bind="err"></rejected-host>
              </template>
            </template>`
            },
            config(),
            wrap('resolved with 42', 'f'),
            getActivationSequenceFor(fhost),
            getDeactivationSequenceFor(fhost),
          );
          yield new TestData(
            'works in nested template - fulfilled>rejected',
            Promise.resolve({ json() { return Promise.reject(new Error('foo-bar')); } }),
            {
              delayPromise, template: `
            <template>
              <template ${pattribute}="promise">
                <pending-host pending p.bind="promise"></pending-host>
                <template ${fattribute}="response" ${pattribute}="response.json()">
                  <fulfilled-host ${fattribute}="data" data.bind="data"></fulfilled-host>
                  <rejected-host ${rattribute}="err" err.bind="updateError(err)"></rejected-host>
                </template>
                <rejected-host ${rattribute}="err" err.bind="err"></rejected-host>
              </template>
            </template>`
            },
            config(),
            '<rejected-host>rejected with foo-bar1</rejected-host>',
            getActivationSequenceFor(rhost),
            getDeactivationSequenceFor(rhost),
          );
          yield new TestData(
            'works in nested template - rejected>fulfilled',
            () => Promise.reject({ json() { return Promise.resolve(42); } }),
            {
              delayPromise, template: `
            <template>
              <template ${pattribute}="promise">
                <pending-host pending p.bind="promise"></pending-host>
                <fulfilled-host ${fattribute}="data" data.bind="data" ce-id="1"></fulfilled-host>
                <template ${rattribute}="response" ${pattribute}="response.json()">
                  <fulfilled-host ${fattribute}="data" data.bind="data" ce-id="2"></fulfilled-host>
                  <rejected-host ${rattribute}="err" err.bind="err"></rejected-host>
                </template>
              </template>
            </template>`
            },
            config(),
            wrap('resolved with 42', 'f'),
            getActivationSequenceFor(`${fhost}-2`),
            getDeactivationSequenceFor(`${fhost}-2`),
          );
          yield new TestData(
            'works in nested template - rejected>rejected',
            () => Promise.reject({ json() { return Promise.reject(new Error('foo-bar')); } }),
            {
              delayPromise, template: `
            <template>
              <template ${pattribute}="promise">
                <pending-host pending p.bind="promise"></pending-host>
                <fulfilled-host ${fattribute}="data" data.bind="data"></fulfilled-host>
                <template ${rattribute}="response" ${pattribute}="response.json()">
                  <fulfilled-host ${fattribute}="data" data.bind="data"></fulfilled-host>
                  <rejected-host ${rattribute}="err" err.bind="err"></rejected-host>
                </template>
              </template>
            </template>`
            },
            config(),
            wrap('rejected with foo-bar', 'r'),
            getActivationSequenceFor(rhost),
            getDeactivationSequenceFor(rhost),
          );

          for (const $resolve of [true, false]) {
            yield new TestData(
              `works with value converter on - settled promise - ${$resolve ? 'fulfilled' : 'rejected'}`,
              null,
              {
                delayPromise, template: `
            <template>
              <template ${pattribute}="42|promisify:${$resolve}">
                <pending-host pending></pending-host>
                <fulfilled-host ${fattribute}="data | double" data.bind="data"></fulfilled-host>
                <rejected-host ${rattribute}="err | double" err.bind="err"></rejected-host>
              </template>
            </template>`
              },
              config(),
              $resolve ? wrap('resolved with 42 42', 'f') : wrap('rejected with 42 42', 'r'),
              getActivationSequenceFor($resolve ? fhost : rhost),
              getDeactivationSequenceFor($resolve ? fhost : rhost),
            );

            yield new TestData(
              `works with value converter - longer running promise - ${$resolve ? 'fulfilled' : 'rejected'}`,
              null,
              {
                delayPromise, template: `
            <template>
              <template ${pattribute}="42|promisify:${$resolve}:25">
                <pending-host pending></pending-host>
                <fulfilled-host ${fattribute}="data | double" data.bind="data"></fulfilled-host>
                <rejected-host ${rattribute}="err | double" err.bind="err"></rejected-host>
              </template>
            </template>`
              },
              config(),
              null,
              null,
              getDeactivationSequenceFor($resolve ? fhost : rhost),
              async (ctx) => {
                const q = ctx.platform.domQueue;
                await q.yield();
                const tc = (ctx.app as ICustomElementViewModel).$controller.children.find((c) => c.viewModel instanceof PromiseTemplateController).viewModel as PromiseTemplateController;
                try {
                  await tc.value;
                } catch {
                  // ignore rejection
                }
                await q.yield();

                if ($resolve) {
                  assert.html.innerEqual(ctx.host, wrap('resolved with 42 42', 'f'), 'fulfilled');
                } else {
                  assert.html.innerEqual(ctx.host, wrap('rejected with 42 42', 'r'), 'rejected');
                }
                ctx.assertCallSet([...getActivationSequenceFor(phost), ...getDeactivationSequenceFor(phost), ...getActivationSequenceFor($resolve ? fhost : rhost)]);
              },
            );

            yield new TestData(
              `works with binding behavior - settled promise - ${$resolve ? 'fulfilled' : 'rejected'}`,
              () => $resolve ? Promise.resolve(42) : Promise.reject(new Error('foo-bar')),
              {
                delayPromise, template: `
            <template>
              <template ${pattribute}="promise & noop">
                <pending-host pending></pending-host>
                <fulfilled-host ${fattribute}="data & noop" data.bind="data"></fulfilled-host>
                <rejected-host ${rattribute}="err & noop" err.bind="err"></rejected-host>
              </template>
            </template>`
              },
              config(),
              $resolve ? wrap('resolved with 42', 'f') : wrap('rejected with foo-bar', 'r'),
              getActivationSequenceFor($resolve ? fhost : rhost),
              getDeactivationSequenceFor($resolve ? fhost : rhost),
            );

            yield new TestData(
              `works with binding behavior - longer running promise - ${$resolve ? 'fulfilled' : 'rejected'}`,
              () => Object.assign(
                createMultiTickPromise(20, () => $resolve ? Promise.resolve(42) : Promise.reject(new Error('foo-bar')))(),
                { id: 0 }
              ),
              {
                delayPromise, template: `
            <template>
              <template ${pattribute}="promise & noop">
                <pending-host pending p.bind="promise"></pending-host>
                <fulfilled-host ${fattribute}="data & noop" data.bind="data"></fulfilled-host>
                <rejected-host ${rattribute}="err & noop" err.bind="err"></rejected-host>
              </template>
            </template>`
              },
              config(),
              wrap('pending0', 'p'),
              getActivationSequenceFor(phost),
              getDeactivationSequenceFor($resolve ? fhost : rhost),
              async (ctx) => {
                ctx.clear();
                const q = ctx.platform.domQueue;
                const tc = (ctx.app as ICustomElementViewModel).$controller.children.find((c) => c.viewModel instanceof PromiseTemplateController).viewModel as PromiseTemplateController;
                try {
                  await tc.value;
                } catch {
                  // ignore rejection
                }
                await q.yield();

                if ($resolve) {
                  assert.html.innerEqual(ctx.host, wrap('resolved with 42', 'f'), 'fulfilled');
                } else {
                  assert.html.innerEqual(ctx.host, wrap('rejected with foo-bar', 'r'), 'rejected');
                }
                ctx.assertCallSet([...getDeactivationSequenceFor(phost), ...getActivationSequenceFor($resolve ? fhost : rhost)]);
              },
            );

            {
              const staticPart = '<my-el>Fizz Bazz</my-el>';
              let resolve: (value: unknown) => void;
              let reject: (value: unknown) => void;
              yield new TestData(
                `enables showing rest of the content although the promise is no settled - ${$resolve ? 'fulfilled' : 'rejected'}`,
                Object.assign(new Promise((rs, rj) => { resolve = rs; reject = rj; }), { id: 0 }),
                {
                  delayPromise, template: `
              <let foo-bar.bind="'Fizz Bazz'"></let>
              <my-el prop.bind="fooBar"></my-el>
              <template ${pattribute}="promise">
                <pending-host pending p.bind="promise"></pending-host>
                <fulfilled-host ${fattribute}="data" data.bind="data"></fulfilled-host>
                <rejected-host ${rattribute}="err" err.bind="err"></rejected-host>
              </template>`,
                  registrations: [
                    CustomElement.define({ name: 'my-el', template: `\${prop}`, bindables: ['prop'] }, class MyEl { }),
                  ]
                },
                config(),
                `${staticPart} ${wrap('pending0', 'p')}`,
                getActivationSequenceFor(phost),
                getDeactivationSequenceFor($resolve ? fhost : rhost),
                async (ctx) => {
                  ctx.clear();
                  if ($resolve) {
                    resolve(42);
                  } else {
                    reject(new Error('foo-bar'));
                  }
                  const p = ctx.platform;
                  // one tick to call back the fulfill delegate, and queue task
                  await p.domQueue.yield();
                  // on the next tick wait the queued task
                  await p.domQueue.yield();
                  assert.html.innerEqual(ctx.host, `${staticPart} ${$resolve ? wrap('resolved with 42', 'f') : wrap('rejected with foo-bar', 'r')}`);
                  ctx.assertCallSet([...getDeactivationSequenceFor(phost), ...getActivationSequenceFor($resolve ? fhost : rhost)]);
                }
              );
            }
          }

          yield new TestData(
            `shows content specific to promise`,
            null,
            {
              delayPromise, template: `
          <template>
            <template ${pattribute}="42|promisify:true">
              <pending-host pending></pending-host>
              <fulfilled-host ${fattribute}="data" data.bind="data"></fulfilled-host>
              <rejected-host ${rattribute}="err" err.bind="err" ce-id="1"></rejected-host>
            </template>

            <template ${pattribute}="'forty-two'|promisify:false">
              <pending-host pending></pending-host>
              <fulfilled-host ${fattribute}="data" data.bind="data"></fulfilled-host>
              <rejected-host ${rattribute}="err" err.bind="err" ce-id="2"></rejected-host>
            </template>
          </template>`
            },
            config(),
            `${wrap('resolved with 42', 'f')} ${wrap('rejected with forty-two', 'r')}`,
            getActivationSequenceFor([fhost, `${rhost}-2`]),
            getDeactivationSequenceFor([fhost, `${rhost}-2`]),
          );

          yield new TestData(
            `[repeat.for] > [${pattribute}] works`,
            null,
            {
              delayPromise,
              template: `
          <template>
            <let items.bind="[[42, true], ['foo-bar', false], ['forty-two', true], ['fizz-bazz', false]]"></let>
            <template repeat.for="item of items">
              <template ${pattribute}="item[0] | promisify:item[1]">
                <let data.bind="null" err.bind="null"></let>
                <fulfilled-host ${fattribute}="data" data.bind="data" ce-id.bind="$index + 1"></fulfilled-host>
                <rejected-host ${rattribute}="err" err.bind="err" ce-id.bind="$index + 1"></rejected-host>
              </template>
            </template>
          </template>`,
            },
            config(),
            `${wrap('resolved with 42', 'f')} ${wrap('rejected with foo-bar', 'r')} ${wrap('resolved with forty-two', 'f')} ${wrap('rejected with fizz-bazz', 'r')}`, //
            getActivationSequenceFor([`${fhost}-1`, `${rhost}-2`, `${fhost}-3`, `${rhost}-4`]),
            getDeactivationSequenceFor([`${fhost}-1`, `${rhost}-2`, `${fhost}-3`, `${rhost}-4`]),
          );

          yield new TestData(
            `[repeat.for,${pattribute}] works`,
            null,
            {
              delayPromise,
              template: `
          <template>
            <let items.bind="[[42, true], ['foo-bar', false], ['forty-two', true], ['fizz-bazz', false]]"></let>
              <template repeat.for="item of items" ${pattribute}="item[0] | promisify:item[1]">
                <let data.bind="null" err.bind="null"></let>
                <fulfilled-host ${fattribute}="data" data.bind="data" ce-id.bind="$index + 1"></fulfilled-host>
                <rejected-host ${rattribute}="err" err.bind="err" ce-id.bind="$index + 1"></rejected-host>
              </template>
          </template>`,
            },
            config(),
            `${wrap('resolved with 42', 'f')} ${wrap('rejected with foo-bar', 'r')} ${wrap('resolved with forty-two', 'f')} ${wrap('rejected with fizz-bazz', 'r')}`,
            getActivationSequenceFor([`${fhost}-1`, `${rhost}-2`, `${fhost}-3`, `${rhost}-4`]),
            getDeactivationSequenceFor([`${fhost}-1`, `${rhost}-2`, `${fhost}-3`, `${rhost}-4`]),
          );

          yield new TestData(
            `[then,repeat.for] works`,
            null,
            {
              delayPromise, template: `
          <template>
            <template ${pattribute}="[42, 'forty-two'] | promisify:true">
              <fulfilled-host ${fattribute}="items" repeat.for="data of items" data.bind="data" ce-id.bind="$index+1"></fulfilled-host>
              <rejected-host ${rattribute}="err" err.bind="err"></rejected-host>
            </template>
          </template>`,
            },
            config(),
            `${wrap('resolved with 42', 'f')}${wrap('resolved with forty-two', 'f')}`,
            getActivationSequenceFor([`${fhost}-1`, `${fhost}-2`]),
            getDeactivationSequenceFor([`${fhost}-1`, `${fhost}-2`]),
          );

          yield new TestData(
            `[then] > [repeat.for] works`,
            null,
            {
              delayPromise, template: `
          <template>
            <template ${pattribute}="[42, 'forty-two'] | promisify:true">
              <template ${fattribute}="items">
                <fulfilled-host repeat.for="data of items" data.bind="data" ce-id.bind="$index+1"></fulfilled-host>
              </template>
              <rejected-host ${rattribute}="err" err.bind="err"></rejected-host>
            </template>
          </template>`,
            },
            config(),
            `${wrap('resolved with 42', 'f')}${wrap('resolved with forty-two', 'f')}`,
            getActivationSequenceFor([`${fhost}-1`, `${fhost}-2`]),
            getDeactivationSequenceFor([`${fhost}-1`, `${fhost}-2`]),
          );
          {
            const registrations = [
              createComponentType('rej-host', `rejected with \${err}`, ['err']),
              ValueConverter.define(
                'parseError',
                class ParseError {
                  public toView(value: Error): string[] {
                    return value.message.split(',');
                  }
                }
              )
            ];
            yield new TestData(
              `[catch,repeat.for] works`,
              null,
              {
                delayPromise, template: `
          <template>
            <template ${pattribute}="[42, 'forty-two'] | promisify:false">
              <fulfilled-host ${fattribute}="data" data.bind="data"></fulfilled-host>
              <rej-host ${rattribute}="error" repeat.for="err of error | parseError" err.bind="err" ce-id.bind="$index + 1"></rej-host>
            </template>
          </template>`,
                registrations,
              },
              config(),
              '<rej-host>rejected with 42</rej-host><rej-host>rejected with forty-two</rej-host>',
              getActivationSequenceFor(['rej-host-1', 'rej-host-2']),
              getDeactivationSequenceFor(['rej-host-1', 'rej-host-2']),
            );
            yield new TestData(
              `[catch] > [repeat.for] works`,
              null,
              {
                delayPromise, template: `
          <template>
            <template ${pattribute}="[42, 'forty-two'] | promisify:false">
              <fulfilled-host ${fattribute}="data" data.bind="data"></fulfilled-host>
              <template ${rattribute}="error">
                <rej-host repeat.for="err of error | parseError" err.bind="err" ce-id.bind="$index + 1"></rej-host>
              </template>
            </template>
          </template>`,
                registrations,
              },
              config(),
              '<rej-host>rejected with 42</rej-host><rej-host>rejected with forty-two</rej-host>',
              getActivationSequenceFor(['rej-host-1', 'rej-host-2']),
              getDeactivationSequenceFor(['rej-host-1', 'rej-host-2']),
            );
          }

          yield new TestData(
            `[if,${pattribute}], [else,${pattribute}] works`,
            null,
            {
              delayPromise, template: `
          <let flag.bind="false"></let>
          <template if.bind="flag" ${pattribute}="42 | promisify:true">
            <fulfilled-host ${fattribute}="data" data.bind="data" ce-id="1"></fulfilled-host>
            <rejected-host ${rattribute}="err" err.bind="err" ce-id="1"></rejected-host>
          </template>
          <template else ${pattribute}="'forty-two' | promisify:false">
            <fulfilled-host ${fattribute}="data" data.bind="data" ce-id="2"></fulfilled-host>
            <rejected-host ${rattribute}="err" err.bind="err" ce-id="2"></rejected-host>
          </template>`,
            },
            config(),
            wrap('rejected with forty-two', 'r'),
            getActivationSequenceFor(`${rhost}-2`),
            getDeactivationSequenceFor(`${fhost}-1`),
            async (ctx) => {
              const q = ctx.platform.domQueue;
              const app = ctx.app;
              const controller = (app as ICustomElementViewModel).$controller;
              const $if = controller.children.find((c) => c.viewModel instanceof If).viewModel as If;
              ctx.clear();

              controller.scope.overrideContext.flag = true;
              await $if['pending'];
              const ptc1 = $if.ifView.children.find((c) => c.viewModel instanceof PromiseTemplateController).viewModel as PromiseTemplateController;
              try {
                await ptc1.value;
              } catch {
                // ignore rejection
              }
              await q.yield();

              assert.html.innerEqual(ctx.host, wrap('resolved with 42', 'f'));
              ctx.assertCallSet([...getDeactivationSequenceFor(`${rhost}-2`), ...getActivationSequenceFor(`${fhost}-1`)]);
            },
          );

          yield new TestData(
            `[pending,if] works`,
            Object.assign(new Promise(() => {/* noop */ }), { id: 0 }),
            {
              delayPromise, template: `
          <let flag.bind="false"></let>
          <template ${pattribute}="promise">
            <pending-host pending p.bind="promise" if.bind="flag"></pending-host>
            <fulfilled-host ${fattribute}="data" data.bind="data"></fulfilled-host>
            <rejected-host ${rattribute}="err" err.bind="err"></rejected-host>
          </template>`,
            },
            config(),
            '',
            [],
            getDeactivationSequenceFor(phost),
            async (ctx) => {
              ctx.clear();
              const q = ctx.platform.domQueue;
              const app = ctx.app;
              const controller = (app as ICustomElementViewModel).$controller;
              const tc = controller.children.find((c) => c.viewModel instanceof PromiseTemplateController).viewModel as PromiseTemplateController;

              controller.scope.overrideContext.flag = true;
              await ((tc['pending']['view'] as ISyntheticView).children.find((c) => c.viewModel instanceof If).viewModel as If)['pending'];
              await q.yield();

              assert.html.innerEqual(ctx.host, wrap('pending0', 'p'));
              ctx.assertCallSet(getActivationSequenceFor(phost));
            },
          );

          yield new TestData(
            `[then,if] works- #1`,
            Object.assign(Promise.resolve(42), { id: 0 }),
            {
              delayPromise, template: `
          <let flag.bind="false"></let>
          <template ${pattribute}="promise">
            <pending-host pending p.bind="promise"></pending-host>
            <fulfilled-host ${fattribute}="data" if.bind="flag" data.bind="data"></fulfilled-host>
            <rejected-host ${rattribute}="err" err.bind="err"></rejected-host>
          </template>`,
            },
            config(),
            '',
            [],
            getDeactivationSequenceFor(fhost),
            async (ctx) => {
              ctx.clear();
              const app = ctx.app;
              const controller = (app as ICustomElementViewModel).$controller;
              const tc = controller.children.find((c) => c.viewModel instanceof PromiseTemplateController).viewModel as PromiseTemplateController;

              controller.scope.overrideContext.flag = true;
              await ((tc['fulfilled']['view'] as ISyntheticView).children.find((c) => c.viewModel instanceof If).viewModel as If)['pending'];

              assert.html.innerEqual(ctx.host, wrap('resolved with 42', 'f'));
              ctx.assertCallSet(getActivationSequenceFor(fhost));
            },
          );

          yield new TestData(
            `[then,if] works- #2`,
            Object.assign(Promise.resolve(24), { id: 0 }),
            {
              delayPromise, template: `
          <template>
            <template ${pattribute}="promise">
              <pending-host pending p.bind="promise"></pending-host>
              <fulfilled-host ${fattribute}="data" if.bind="data === 42" data.bind="data"></fulfilled-host>
              <rejected-host ${rattribute}="err" err.bind="err"></rejected-host>
            </template>
          </template>`,
            },
            config(),
            '',
            [],
            getDeactivationSequenceFor(fhost),
            async (ctx) => {
              ctx.clear();
              const q = ctx.platform.domQueue;
              const app = ctx.app;
              await (app.promise = Promise.resolve(42));
              await q.yield();

              const controller = (app as ICustomElementViewModel).$controller;
              const tc = controller.children.find((c) => c.viewModel instanceof PromiseTemplateController).viewModel as PromiseTemplateController;

              controller.scope.overrideContext.flag = true;
              await ((tc['fulfilled']['view'] as ISyntheticView).children.find((c) => c.viewModel instanceof If).viewModel as If)['pending'];

              assert.html.innerEqual(ctx.host, wrap('resolved with 42', 'f'));
              ctx.assertCallSet(getActivationSequenceFor(fhost));
            },
          );

          yield new TestData(
            `[catch,if] works- #1`,
            () => Object.assign(Promise.reject(new Error('foo-bar')), { id: 0 }),
            {
              delayPromise, template: `
          <let flag.bind="false"></let>
          <template ${pattribute}="promise">
            <pending-host pending p.bind="promise"></pending-host>
            <fulfilled-host ${fattribute}="data" data.bind="data"></fulfilled-host>
            <rejected-host ${rattribute}="err" if.bind="flag" err.bind="err"></rejected-host>
          </template>`,
            },
            config(),
            '',
            [],
            getDeactivationSequenceFor(rhost),
            async (ctx) => {
              ctx.clear();
              const app = ctx.app;
              const controller = (app as ICustomElementViewModel).$controller;
              const tc = controller.children.find((c) => c.viewModel instanceof PromiseTemplateController).viewModel as PromiseTemplateController;

              controller.scope.overrideContext.flag = true;
              await ((tc['rejected']['view'] as ISyntheticView).children.find((c) => c.viewModel instanceof If).viewModel as If)['pending'];

              assert.html.innerEqual(ctx.host, wrap('rejected with foo-bar', 'r'));
              ctx.assertCallSet(getActivationSequenceFor(rhost));
            },
          );

          yield new TestData(
            `[catch,if] works- #2`,
            () => Object.assign(Promise.reject(new Error('foo')), { id: 0 }),
            {
              delayPromise, template: `
          <template>
            <template ${pattribute}="promise">
              <pending-host pending p.bind="promise"></pending-host>
              <fulfilled-host ${fattribute}="data" data.bind="data"></fulfilled-host>
              <rejected-host ${rattribute}="err" if.bind="err.message === 'foo-bar'" err.bind="err"></rejected-host>
            </template>
          </template>`,
            },
            config(),
            '',
            [],
            getDeactivationSequenceFor(rhost),
            async (ctx) => {
              ctx.clear();
              const q = ctx.platform.domQueue;
              const app = ctx.app;
              try {
                await (app.promise = Promise.reject(new Error('foo-bar')));
              } catch {
                // ignore rejection
              }
              await q.yield();

              const controller = (app as ICustomElementViewModel).$controller;
              const tc = controller.children.find((c) => c.viewModel instanceof PromiseTemplateController).viewModel as PromiseTemplateController;

              controller.scope.overrideContext.flag = true;
              await ((tc['rejected']['view'] as ISyntheticView).children.find((c) => c.viewModel instanceof If).viewModel as If)['pending'];

              assert.html.innerEqual(ctx.host, wrap('rejected with foo-bar', 'r'));
              ctx.assertCallSet(getActivationSequenceFor(rhost));
            },
          );

          const waitSwitch: ($switch: Switch) => Promise<void> = async ($switch) => {
            const promise = $switch.promise;
            await promise;
            if ($switch.promise !== promise) {
              await waitSwitch($switch);
            }
          };

          for (const $resolve of [true, false]) {

            yield new TestData(
              `[case,${pattribute}] works - ${$resolve ? 'fulfilled' : 'rejected'}`,
              () => $resolve ? Promise.resolve(42) : Promise.reject(new Error('foo-bar')),
              {
                delayPromise, template: `
          <let status.bind="'unknown'"></let>
          <template switch.bind="status">
            <template case="unknown">Unknown</template>
            <template case="processing" ${pattribute}="promise">
              <pending-host pending p.bind="promise"></pending-host>
              <fulfilled-host ${fattribute}="data" data.bind="data"></fulfilled-host>
              <rejected-host ${rattribute}="err" if.bind="err.message === 'foo-bar'" err.bind="err"></rejected-host>
            </template>
          </template>`,
              },
              config(),
              'Unknown',
              [],
              getDeactivationSequenceFor($resolve ? fhost : rhost),
              async (ctx) => {
                ctx.clear();
                const q = ctx.platform.domQueue;
                const app = ctx.app;
                const controller = (app as ICustomElementViewModel).$controller;
                controller.scope.overrideContext.status = 'processing';
                await waitSwitch(controller.children.find((c) => c.viewModel instanceof Switch).viewModel as Switch);

                try {
                  await app.promise;
                } catch {
                  // ignore rejection
                }
                await q.yield();

                assert.html.innerEqual(ctx.host, $resolve ? wrap('resolved with 42', 'f') : wrap('rejected with foo-bar', 'r'));
                ctx.assertCallSet(getActivationSequenceFor($resolve ? fhost : rhost));
              },
            );
          }

          yield new TestData(
            `[then,switch] works - #1`,
            Promise.resolve('foo'),
            {
              delayPromise, template: `
          <template>
            <template ${pattribute}="promise">
              <pending-host pending p.bind="promise"></pending-host>
              <template ${fattribute}="status" switch.bind="status">
                <fulfilled-host case='processing' data="processing" ce-id="1"></fulfilled-host>
                <fulfilled-host default-case data="unknown" ce-id="2"></fulfilled-host>
              </template>
              <rejected-host ${rattribute}="err" err.bind="err"></rejected-host>
            </template>
          </template>`,
            },
            config(),
            '<fulfilled-host>resolved with unknown</fulfilled-host>',
            getActivationSequenceFor(`${fhost}-2`),
            getDeactivationSequenceFor(`${fhost}-1`),
            async (ctx) => {
              ctx.clear();
              const q = ctx.platform.domQueue;
              const app = ctx.app;
              const controller = (app as ICustomElementViewModel).$controller;
              const tc = controller.children.find((c) => c.viewModel instanceof PromiseTemplateController).viewModel as PromiseTemplateController;
              const $switch = tc['fulfilled'].view.children.find((c) => c.viewModel instanceof Switch).viewModel as Switch;

              await (app.promise = Promise.resolve('processing'));
              await q.yield();
              await waitSwitch($switch);

              assert.html.innerEqual(ctx.host, '<fulfilled-host>resolved with processing</fulfilled-host>');
              ctx.assertCallSet([...getDeactivationSequenceFor(`${fhost}-2`), ...getActivationSequenceFor(`${fhost}-1`)]);
            },
          );

          yield new TestData(
            `[then,switch] works - #2`,
            Promise.resolve('foo'),
            {
              delayPromise, template: `
          <let status.bind="'processing'"></let>
          <template ${pattribute}="promise">
            <pending-host pending p.bind="promise"></pending-host>
            <template then switch.bind="status">
              <fulfilled-host case='processing' data="processing" ce-id="1"></fulfilled-host>
              <fulfilled-host default-case data="unknown" ce-id="2"></fulfilled-host>
            </template>
            <rejected-host ${rattribute}="err" err.bind="err"></rejected-host>
          </template>`,
            },
            config(),
            '<fulfilled-host>resolved with processing</fulfilled-host>',
            getActivationSequenceFor(`${fhost}-1`),
            getDeactivationSequenceFor(`${fhost}-2`),
            async (ctx) => {
              ctx.clear();
              const app = ctx.app;
              const controller = (app as ICustomElementViewModel).$controller;
              const tc = controller.children.find((c) => c.viewModel instanceof PromiseTemplateController).viewModel as PromiseTemplateController;
              const $switch = tc['fulfilled'].view.children.find((c) => c.viewModel instanceof Switch).viewModel as Switch;
              controller.scope.overrideContext.status = 'foo';
              await waitSwitch($switch);
              assert.html.innerEqual(ctx.host, '<fulfilled-host>resolved with unknown</fulfilled-host>');
              ctx.assertCallSet([...getDeactivationSequenceFor(`${fhost}-1`), ...getActivationSequenceFor(`${fhost}-2`)]);
            }
          );

          yield new TestData(
            `[catch,switch] works - #1`,
            () => Promise.reject(new Error('foo')),
            {
              delayPromise, template: `
          <template>
            <template ${pattribute}="promise">
              <pending-host pending p.bind="promise"></pending-host>
              <fulfilled-host ${fattribute}="data" data.bind="data"></fulfilled-host>
              <template ${rattribute}="err" switch.bind="err.message">
                <rejected-host case='processing' err.bind="{message: 'processing'}" ce-id="1"></rejected-host>
                <rejected-host default-case  err.bind="{message: 'unknown'}" ce-id="2"></rejected-host>
              </template>
            </template>
          </template>`,
            },
            config(),
            '<rejected-host>rejected with unknown</rejected-host>',
            getActivationSequenceFor(`${rhost}-2`),
            getDeactivationSequenceFor(`${rhost}-1`),
            async (ctx) => {
              ctx.clear();
              const q = ctx.platform.domQueue;
              const app = ctx.app;
              const controller = (app as ICustomElementViewModel).$controller;
              const tc = controller.children.find((c) => c.viewModel instanceof PromiseTemplateController).viewModel as PromiseTemplateController;
              const $switch = tc['rejected'].view.children.find((c) => c.viewModel instanceof Switch).viewModel as Switch;

              try {
                await (app.promise = Promise.reject(new Error('processing')));
              } catch {
                // ignore rejection
              }
              await q.yield();
              await waitSwitch($switch);

              assert.html.innerEqual(
                ctx.host,
                '<rejected-host>rejected with processing</rejected-host>'
              );
              ctx.assertCallSet([...getDeactivationSequenceFor(`${rhost}-2`), ...getActivationSequenceFor(`${rhost}-1`)]);
            },
          );

          yield new TestData(
            `[catch,switch] works - #2`,
            () => Promise.reject(new Error('foo')),
            {
              delayPromise, template: `
          <let status.bind="'processing'"></let>
          <template ${pattribute}="promise">
            <pending-host pending p.bind="promise"></pending-host>
            <fulfilled-host ${fattribute}="data" data.bind="data"></fulfilled-host>
            <template catch switch.bind="status">
              <rejected-host case='processing' err.bind="{message: 'processing'}" ce-id="1"></rejected-host>
              <rejected-host default-case  err.bind="{message: 'unknown'}" ce-id="2"></rejected-host>
            </template>
          </template>`,
            },
            config(),
            '<rejected-host>rejected with processing</rejected-host>',
            getActivationSequenceFor(`${rhost}-1`),
            getDeactivationSequenceFor(`${rhost}-2`),
            async (ctx) => {
              ctx.clear();
              const app = ctx.app;
              const controller = (app as ICustomElementViewModel).$controller;
              const tc = controller.children.find((c) => c.viewModel instanceof PromiseTemplateController).viewModel as PromiseTemplateController;
              const $switch = tc['rejected'].view.children.find((c) => c.viewModel instanceof Switch).viewModel as Switch;
              controller.scope.overrideContext.status = 'foo';
              await waitSwitch($switch);
              assert.html.innerEqual(ctx.host, '<rejected-host>rejected with unknown</rejected-host>');
              ctx.assertCallSet([...getDeactivationSequenceFor(`${rhost}-1`), ...getActivationSequenceFor(`${rhost}-2`)]);
            }
          );

          yield new TestData(
            `au-slot use-case`,
            () => Promise.reject(new Error('foo')),
            {
              delayPromise, template: `
          <foo-bar p.bind="42|promisify:true">
            <div au-slot>f1</div>
            <div au-slot="rejected">r1</div>
          </foo-bar>
          <foo-bar p.bind="'forty-two'|promisify:false">
            <div au-slot>f2</div>
            <div au-slot="rejected">r2</div>
          </foo-bar>
          <template as-custom-element="foo-bar">
            <bindable name="p"></bindable>
            <template ${pattribute}="p">
              <au-slot name="pending" pending></au-slot>
              <au-slot then></au-slot>
              <au-slot name="rejected" catch></au-slot>
            </template>
          </template>`,
            },
            config(),
            '<foo-bar> <div>f1</div> </foo-bar> <foo-bar> <div>r2</div> </foo-bar>',
            [],
            [],
          );
        }

        // #region timings
        for (const $resolve of [true, false]) {
          const getPromise = (ticks: number) => () => Object.assign(
            createMultiTickPromise(ticks, () => $resolve ? Promise.resolve(42) : Promise.reject(new Error('foo-bar')))(),
            { id: 0 }
          );
          yield new TestData(
            `pending activation duration < promise settlement duration - ${$resolve ? 'fulfilled' : 'rejected'}`,
            getPromise(20),
            {
              delayPromise, template: template1,
              registrations: [
                Registration.instance(configLookup, new Map<string, Config>([
                  [phost, new Config(true, createWaiterWithTicks({ binding: 1, bound: 1, attaching: 1, attached: 1 }))],
                  [fhost, new Config(true, createWaiterWithTicks(Object.create(null)))],
                  [rhost, new Config(true, createWaiterWithTicks(Object.create(null)))],
                ])),
              ],
            },
            null,
            null,
            null,
            getDeactivationSequenceFor($resolve ? fhost : rhost),
            async (ctx) => {
              const q = ctx.platform.domQueue;

              await q.yield();
              try {
                await ctx.app.promise;
              } catch (e) {
                // ignore rejection
              }
              await q.yield();

              if ($resolve) {
                assert.html.innerEqual(ctx.host, wrap('resolved with 42', 'f'), 'fulfilled');
              } else {
                assert.html.innerEqual(ctx.host, wrap('rejected with foo-bar', 'r'), 'rejected');
              }
              ctx.assertCallSet([...getActivationSequenceFor(phost), ...getDeactivationSequenceFor(phost), ...getActivationSequenceFor($resolve ? fhost : rhost)]);
            },
          );

          // These tests are more like sanity checks rather than asserting the lifecycle hooks invocation timings and sequence of those.
          // These rather assert that under varied configurations of promise and hook timings, the template controllers still work.
          for (const [name, promiseTick, config] of [
            ['pending activation duration == promise settlement duration', 4, { binding: 1, bound: 1, attaching: 1, attached: 1 }],
            ['pending "binding" duration == promise settlement duration', 2, { binding: 2 }],
            ['pending "binding" duration > promise settlement duration', 1, { binding: 2 }],
            ['pending "binding" duration > promise settlement duration (longer running promise and hook)', 4, { binding: 6 }],
            ['pending "binding+bound" duration > promise settlement duration', 2, { binding: 1, bound: 2 }],
            ['pending "binding+bound" duration > promise settlement duration (longer running promise and hook)', 4, { binding: 3, bound: 3 }],
            ['pending "binding+bound+attaching" duration > promise settlement duration', 2, { binding: 1, bound: 1, attaching: 1 }],
            ['pending "binding+bound+attaching" duration > promise settlement duration (longer running promise and hook)', 5, { binding: 2, bound: 2, attaching: 2 }],
            ['pending "binding+bound+attaching+attached" duration > promise settlement duration', 3, { binding: 1, bound: 1, attaching: 1, attached: 1 }],
            ['pending "binding+bound+attaching+attached" duration > promise settlement duration (longer running promise and hook)', 6, { binding: 2, bound: 2, attaching: 2, attached: 2 }],
          ] as const) {
            yield new TestData(
              `${name} - ${$resolve ? 'fulfilled' : 'rejected'}`,
              getPromise(promiseTick),
              {
                delayPromise, template: template1,
                registrations: [
                  Registration.instance(configLookup, new Map<string, Config>([
                    [phost, new Config(true, createWaiterWithTicks(config))],
                    [fhost, new Config(true, createWaiterWithTicks(Object.create(null)))],
                    [rhost, new Config(true, createWaiterWithTicks(Object.create(null)))],
                  ])),
                ],
              },
              null,
              null,
              null,
              getDeactivationSequenceFor($resolve ? fhost : rhost),
              async (ctx) => {
                const q = ctx.platform.domQueue;
                await q.yield();

                const app = ctx.app;
                // Note: If the ticks are close to each other, we cannot avoid a race condition for the purpose of deterministic tests.
                // Therefore, the expected logs are constructed dynamically to ensure certain level of confidence.
                const tc = (app as ICustomElementViewModel).$controller.children.find((c) => c.viewModel instanceof PromiseTemplateController).viewModel as PromiseTemplateController;
                const task = tc['preSettledTask'] as (Task<void | Promise<void>> | null);
                const logs = task.status === 'running' || task.status === 'completed'
                  ? [...getActivationSequenceFor(phost), ...getDeactivationSequenceFor(phost)]
                  : [];

                try {
                  await app.promise;
                } catch {
                  // ignore rejection
                }

                await q.yield();
                if ($resolve) {
                  assert.html.innerEqual(ctx.host, wrap('resolved with 42', 'f'), 'fulfilled');
                } else {
                  assert.html.innerEqual(ctx.host, wrap('rejected with foo-bar', 'r'), 'rejected');
                }
                ctx.assertCallSet([...logs, ...getActivationSequenceFor($resolve ? fhost : rhost)], `calls mismatch; presettled task status: ${task.status}`);
              }
            );
          }

          yield new TestData(
            `change of promise in quick succession - final promise is settled - ${$resolve ? 'fulfilled' : 'rejected'}`,
            Promise.resolve(42),
            {
              delayPromise, template: template1,
              registrations: [
                Registration.instance(configLookup, new Map<string, Config>([
                  [phost, new Config(true, createWaiterWithTicks(Object.create(null)))],
                  [fhost, new Config(true, createWaiterWithTicks(Object.create(null)))],
                  [rhost, new Config(true, createWaiterWithTicks(Object.create(null)))],
                ])),
              ],
            },
            null,
            wrap(`resolved with 42`, 'f'),
            getActivationSequenceFor(fhost),
            getDeactivationSequenceFor($resolve ? fhost : rhost),
            async (ctx) => {
              ctx.clear();
              const app = ctx.app;
              app.promise = Object.assign(new Promise(() => { /* unsettled */ }), { id: 0 });

              const q = ctx.platform.domQueue;
              await q.yield();
              assert.html.innerEqual(ctx.host, wrap('pending0', 'p'), 'pending');
              ctx.assertCallSet([...getDeactivationSequenceFor(fhost), ...getActivationSequenceFor(phost)], `calls mismatch1`);
              ctx.clear();

              try {
                // interrupt
                await (app.promise = $resolve ? Promise.resolve(4242) : Promise.reject(new Error('foo-bar foo-bar')));
              } catch {
                // ignore rejection
              }
              // wait for the next tick
              await q.yield();
              if ($resolve) {
                assert.html.innerEqual(ctx.host, wrap('resolved with 4242', 'f'), 'fulfilled');
              } else {
                assert.html.innerEqual(ctx.host, wrap('rejected with foo-bar foo-bar', 'r'), 'rejected');
              }
              ctx.assertCallSet([...getDeactivationSequenceFor(phost), ...getActivationSequenceFor($resolve ? fhost : rhost)], `calls mismatch2`);
            },
          );

          yield new TestData(
            `change of promise in quick succession - final promise is of shorter duration - ${$resolve ? 'fulfilled' : 'rejected'}`,
            Promise.resolve(42),
            {
              delayPromise, template: template1,
              registrations: [
                Registration.instance(configLookup, new Map<string, Config>([
                  [phost, new Config(true, createWaiterWithTicks(Object.create(null)))],
                  [fhost, new Config(true, createWaiterWithTicks(Object.create(null)))],
                  [rhost, new Config(true, createWaiterWithTicks(Object.create(null)))],
                ])),
              ],
            },
            null,
            wrap(`resolved with 42`, 'f'),
            getActivationSequenceFor(fhost),
            getDeactivationSequenceFor($resolve ? fhost : rhost),
            async (ctx) => {
              ctx.clear();
              const app = ctx.app;
              app.promise = Object.assign(new Promise(() => { /* unsettled */ }), { id: 0 });

              const q = ctx.platform.domQueue;
              await q.yield();
              assert.html.innerEqual(ctx.host, wrap('pending0', 'p'), 'pending0');
              ctx.assertCallSet([...getDeactivationSequenceFor(fhost), ...getActivationSequenceFor(phost)], `calls mismatch1`);
              ctx.clear();

              // interrupt; the previous promise is of longer duration because it is never settled.
              const promise = app.promise = Object.assign(
                createMultiTickPromise(5, () => $resolve ? Promise.resolve(4242) : Promise.reject(new Error('foo-bar foo-bar')))(),
                { id: 1 }
              );

              await q.queueTask(() => {
                assert.html.innerEqual(ctx.host, wrap('pending1', 'p'), 'pending1');
              }).result;
              ctx.assertCallSet([], `calls mismatch2`);
              ctx.clear();

              try {
                await promise;
              } catch {
                // ignore rejection
              }
              // wait for the next tick
              await q.yield();

              if ($resolve) {
                assert.html.innerEqual(ctx.host, wrap('resolved with 4242', 'f'), 'fulfilled');
              } else {
                assert.html.innerEqual(ctx.host, wrap('rejected with foo-bar foo-bar', 'r'), 'rejected');
              }
              ctx.assertCallSet([...getDeactivationSequenceFor(phost), ...getActivationSequenceFor($resolve ? fhost : rhost)], `calls mismatch2`);
            },
          );

          yield new TestData(
            `change of promise in quick succession - changed after previous promise is settled but the post-settlement activation is pending - ${$resolve ? 'fulfilled' : 'rejected'}`,
            Promise.resolve(42),
            {
              delayPromise, template: template1,
              registrations: [
                Registration.instance(configLookup, new Map<string, Config>([
                  [phost, new Config(true, createWaiterWithTicks(Object.create(null)))],
                  [fhost, new Config(true, createWaiterWithTicks($resolve ? { binding: 1, bound: 2, attaching: 2, attached: 2 } : Object.create(null)))],
                  [rhost, new Config(true, createWaiterWithTicks($resolve ? Object.create(null) : { binding: 1, bound: 2, attaching: 2, attached: 2 }))],
                ])),
              ],
            },
            null,
            wrap(`resolved with 42`, 'f'),
            getActivationSequenceFor(fhost),
            getDeactivationSequenceFor($resolve ? fhost : rhost),
            async (ctx) => {
              ctx.clear();
              const app = ctx.app;
              let resolve: (value: unknown) => void;
              let reject: (value: unknown) => void;
              let promise = app.promise = Object.assign(new Promise((rs, rj) => [resolve, reject] = [rs, rj]), { id: 0 });

              const q = ctx.platform.domQueue;
              await q.yield();
              assert.html.innerEqual(ctx.host, wrap('pending0', 'p'), 'pending0');
              ctx.assertCallSet([...getDeactivationSequenceFor(fhost), ...getActivationSequenceFor(phost)], `calls mismatch1`);
              ctx.clear();

              try {
                if ($resolve) {
                  resolve(84);
                } else {
                  reject(new Error('fizz bazz'));
                }
                await promise;
              } catch {
                // ignore rejection
              }

              // attempt interrupt
              promise = app.promise = Object.assign(
                createMultiTickPromise(20, () => $resolve ? Promise.resolve(4242) : Promise.reject(new Error('foo-bar foo-bar')))(),
                { id: 1 }
              );

              await q.yield();
              assert.html.innerEqual(ctx.host, wrap('pending1', 'p'), 'pending1');
              ctx.assertCallSet([], `calls mismatch3`);
              ctx.clear();

              try {
                await promise;
              } catch {
                // ignore rejection
              }
              await q.yield();

              if ($resolve) {
                assert.html.innerEqual(ctx.host, wrap('resolved with 4242', 'f'), 'fulfilled 2');
              } else {
                assert.html.innerEqual(ctx.host, wrap('rejected with foo-bar foo-bar', 'r'), 'rejected 2');
              }
              ctx.assertCallSet([...getDeactivationSequenceFor(phost), ...getActivationSequenceFor($resolve ? fhost : rhost)], `calls mismatch4`);
            },
          );

          yield new TestData(
            `change of promise in quick succession - changed after the post-settlement activation is running - ${$resolve ? 'fulfilled' : 'rejected'}`,
            Promise.resolve(42),
            {
              delayPromise, template: template1,
              registrations: [
                Registration.instance(configLookup, new Map<string, Config>([
                  [phost, new Config(true, createWaiterWithTicks(Object.create(null)))],
                  [fhost, new Config(true, createWaiterWithTicks($resolve ? { binding: 1, bound: 2, attaching: 2, attached: 2 } : Object.create(null)))],
                  [rhost, new Config(true, createWaiterWithTicks($resolve ? Object.create(null) : { binding: 1, bound: 2, attaching: 2, attached: 2 }))],
                ])),
              ],
            },
            null,
            wrap(`resolved with 42`, 'f'),
            getActivationSequenceFor(fhost),
            getDeactivationSequenceFor($resolve ? fhost : rhost),
            async (ctx) => {
              ctx.clear();
              const app = ctx.app;

              let resolve: (value: unknown) => void;
              let reject: (value: unknown) => void;
              let promise = app.promise = Object.assign(new Promise((rs, rj) => [resolve, reject] = [rs, rj]), { id: 0 });

              const q = ctx.platform.domQueue;
              await q.yield();
              assert.html.innerEqual(ctx.host, wrap('pending0', 'p'), 'pending0');
              ctx.assertCallSet([...getDeactivationSequenceFor(fhost), ...getActivationSequenceFor(phost)], `calls mismatch1`);
              ctx.clear();

              try {
                if ($resolve) {
                  resolve(84);
                } else {
                  reject(new Error('foo-bar'));
                }
                await promise;
              } catch {
                // ignore rejection
              }

              // run the post-settled task
              q.flush();
              promise = app.promise = Object.assign(new Promise((rs, rj) => [resolve, reject] = [rs, rj]), { id: 1 });

              await q.yield();
              if ($resolve) {
                assert.html.innerEqual(ctx.host, wrap('resolved with 84', 'f'), 'fulfilled 1');
              } else {
                assert.html.innerEqual(ctx.host, wrap('rejected with foo-bar', 'r'), 'rejected 1');
              }
              ctx.assertCallSet([...getDeactivationSequenceFor(phost), ...getActivationSequenceFor($resolve ? fhost : rhost)], `calls mismatch2`);
              ctx.clear();

              await q.yield();
              assert.html.innerEqual(ctx.host, wrap('pending1', 'p'), 'pending1');
              ctx.assertCallSet([...getDeactivationSequenceFor($resolve ? fhost : rhost), ...getActivationSequenceFor(phost)], `calls mismatch3`);
              ctx.clear();

              try {
                if ($resolve) {
                  resolve(4242);
                } else {
                  reject(new Error('foo-bar foo-bar'));
                }
                await promise;
              } catch {
                // ignore rejection
              }
              await q.yield();

              if ($resolve) {
                assert.html.innerEqual(ctx.host, wrap('resolved with 4242', 'f'), 'fulfilled 2');
              } else {
                assert.html.innerEqual(ctx.host, wrap('rejected with foo-bar foo-bar', 'r'), 'rejected 2');
              }
              ctx.assertCallSet([...getDeactivationSequenceFor(phost), ...getActivationSequenceFor($resolve ? fhost : rhost)], `calls mismatch4`);
            },
          );

          yield new TestData(
            `change of promise in quick succession - previous promise is settled after the new promise is settled - ${$resolve ? 'fulfilled' : 'rejected'}`,
            Promise.resolve(42),
            {
              delayPromise, template: template1,
              registrations: [
                Registration.instance(configLookup, new Map<string, Config>([
                  [phost, new Config(true, createWaiterWithTicks(Object.create(null)))],
                  [fhost, new Config(true, createWaiterWithTicks($resolve ? { binding: 1, bound: 2, attaching: 2, attached: 2 } : Object.create(null)))],
                  [rhost, new Config(true, createWaiterWithTicks($resolve ? Object.create(null) : { binding: 1, bound: 2, attaching: 2, attached: 2 }))],
                ])),
              ],
            },
            null,
            wrap(`resolved with 42`, 'f'),
            getActivationSequenceFor(fhost),
            getDeactivationSequenceFor($resolve ? fhost : rhost),
            async (ctx) => {
              ctx.clear();
              const app = ctx.app;

              let resolve1: (value: unknown) => void;
              let reject1: (value: unknown) => void;
              const promise1 = app.promise = Object.assign(new Promise((rs, rj) => [resolve1, reject1] = [rs, rj]), { id: 0 });

              const q = ctx.platform.domQueue;
              await q.yield();
              assert.html.innerEqual(ctx.host, wrap('pending0', 'p'), 'pending0');
              ctx.assertCallSet([...getDeactivationSequenceFor(fhost), ...getActivationSequenceFor(phost)], `calls mismatch1`);
              ctx.clear();

              try {
                await (app.promise = Object.assign($resolve ? Promise.resolve(84) : Promise.reject(new Error('foo-bar')), { id: 1 }));
              } catch {
                // ignore rejection
              }

              try {
                if ($resolve) {
                  resolve1(4242);
                } else {
                  reject1(new Error('fiz baz'));
                }
                await promise1;
              } catch {
                // ignore rejection
              }

              await q.yield();
              if ($resolve) {
                assert.html.innerEqual(ctx.host, wrap('resolved with 84', 'f'), 'fulfilled 1');
              } else {
                assert.html.innerEqual(ctx.host, wrap('rejected with foo-bar', 'r'), 'rejected 1');
              }
              ctx.assertCallSet([...getDeactivationSequenceFor(phost), ...getActivationSequenceFor($resolve ? fhost : rhost)], `calls mismatch2`);
            },
          );

          yield new TestData(
            `change of promise in quick succession - previous promise is settled after the new post-settled work is finished - ${$resolve ? 'fulfilled' : 'rejected'}`,
            Promise.resolve(42),
            {
              delayPromise, template: template1,
              registrations: [
                Registration.instance(configLookup, new Map<string, Config>([
                  [phost, new Config(true, createWaiterWithTicks(Object.create(null)))],
                  [fhost, new Config(true, createWaiterWithTicks($resolve ? { binding: 1, bound: 2, attaching: 2, attached: 2 } : Object.create(null)))],
                  [rhost, new Config(true, createWaiterWithTicks($resolve ? Object.create(null) : { binding: 1, bound: 2, attaching: 2, attached: 2 }))],
                ])),
              ],
            },
            null,
            wrap(`resolved with 42`, 'f'),
            getActivationSequenceFor(fhost),
            getDeactivationSequenceFor($resolve ? fhost : rhost),
            async (ctx) => {
              ctx.clear();
              const app = ctx.app;

              let resolve1: (value: unknown) => void;
              let reject1: (value: unknown) => void;
              const promise1 = app.promise = Object.assign(new Promise((rs, rj) => [resolve1, reject1] = [rs, rj]), { id: 0 });

              const q = ctx.platform.domQueue;
              await q.yield();
              assert.html.innerEqual(ctx.host, wrap('pending0', 'p'), 'pending0');
              ctx.assertCallSet([...getDeactivationSequenceFor(fhost), ...getActivationSequenceFor(phost)], `calls mismatch1`);
              ctx.clear();

              try {
                await (app.promise = Object.assign($resolve ? Promise.resolve(84) : Promise.reject(new Error('foo-bar')), { id: 1 }));
              } catch {
                // ignore rejection
              }

              await q.yield();
              if ($resolve) {
                assert.html.innerEqual(ctx.host, wrap('resolved with 84', 'f'), 'fulfilled 1');
              } else {
                assert.html.innerEqual(ctx.host, wrap('rejected with foo-bar', 'r'), 'rejected 1');
              }
              ctx.assertCallSet([...getDeactivationSequenceFor(phost), ...getActivationSequenceFor($resolve ? fhost : rhost)], `calls mismatch2`);

              const tc = (ctx.app as ICustomElementViewModel).$controller.children.find((c) => c.viewModel instanceof PromiseTemplateController).viewModel as PromiseTemplateController;
              const postSettleTask = tc['postSettledTask'];
              let { pending, processing, delayed } = reportTaskQueue(q);
              const taskNums = [pending.length, processing.length, delayed.length];

              try {
                if ($resolve) {
                  resolve1(4242);
                } else {
                  reject1(new Error('fiz baz'));
                }
                await promise1;
              } catch {
                // ignore rejection
              }
              await q.yield();
              assert.strictEqual(tc['postSettledTask'], postSettleTask);
              ({ pending, processing, delayed } = reportTaskQueue(q));
              assert.deepStrictEqual([pending.length, processing.length, delayed.length], taskNums);
            },
          );
        }
        // #endregion
      }

      // #region scope
      for (const $resolve of [true, false]) {
        {
          class App1 {
            public readonly promise: Promise<number>;
            public data: number;
            public err: Error;
            public constructor() {
              this.promise = $resolve ? Promise.resolve(42) : Promise.reject(new Error('foo-bar'));
            }
            public async binding(): Promise<void> {
              try {
                this.data = (await this.promise) ** 2;
              } catch (e) {
                this.err = new Error(`modified ${e.message}`);
              }
            }
          }

          yield new TestData<App1>(
            `shows scoped content correctly #1 - ${$resolve ? 'fulfilled' : 'rejected'}`,
            null,
            {
              template: `
              <div ${pattribute}="promise">
                <let data.bind="null" err.bind="null"></let>
                <div ${fattribute}="data">\${data} \${$parent.data}</div>
                <div ${rattribute}="err">'\${err.message}' '\${$parent.err.message}'</div>
              </div>`,
              appType: App1,
            },
            null,
            $resolve
              ? `<div> <div>42 1764</div> </div>`
              : `<div> <div>'foo-bar' 'modified foo-bar'</div> </div>`,
            [],
            [],
            // void 0,
            // true
          );
          yield new TestData<App1>(
            `shows scoped content correctly #2 - ${$resolve ? 'fulfilled' : 'rejected'}`,
            null,
            {
              template: `
              <div ${pattribute}="promise">
                <div ${fattribute}>\${data}</div>
                <div ${rattribute}>\${err.message}</div>
              </div>`,
              appType: App1,
            },
            null,
            $resolve
              ? `<div> <div>1764</div> </div>`
              : `<div> <div>modified foo-bar</div> </div>`,
            [],
            [],
          );
        }
        {
          class App1 implements ICustomElementViewModel {
            public readonly promise: Promise<number>;
            public data: number;
            public err: Error;
            public $controller: ICustomElementController<this>;
            public constructor() {
              this.promise = $resolve ? Promise.resolve(42) : Promise.reject(new Error('foo-bar'));
            }
          }

          yield new TestData<App1>(
            `shows scoped content correctly #3 - ${$resolve ? 'fulfilled' : 'rejected'}`,
            null,
            {
              template: `
              <div ${pattribute}="promise">
                <div ${fattribute}="$parent.data">\${data} \${$parent.data}</div>
                <div ${rattribute}="$parent.err">'\${err.message}' '\${$parent.err.message}'</div>
              </div>
              \${data} \${err.message}`,
              appType: App1,
            },
            null,
            $resolve
              ? `<div> <div>42 42</div> </div> 42`
              : `<div> <div>'foo-bar' 'foo-bar'</div> </div> foo-bar`,
            [],
            [],
            (ctx) => {
              const app = ctx.app;
              const s = app.$controller.scope;
              const bc = s.bindingContext;
              const oc = s.overrideContext;
              if ($resolve) {
                assert.strictEqual(bc.data, 42, 'bc.data');
                assert.strictEqual(bc.err, undefined, 'bc.err');
              } else {
                assert.strictEqual(bc.data, undefined, 'bc.data');
                assert.strictEqual((bc.err as Error).message, 'foo-bar', 'bc.err');
              }
              assert.strictEqual('data' in oc, false, 'data in oc');
              assert.strictEqual('err' in oc, false, 'err in oc');
            }
          );

          yield new TestData<App1>(
            `shows scoped content correctly #4 - ${$resolve ? 'fulfilled' : 'rejected'}`,
            null,
            {
              template: `
              <div ${pattribute}="promise">
                <div ${fattribute}="data">\${data}</div>
                <div ${rattribute}="err">\${err.message}</div>
              </div>`,
              appType: App1,
            },
            null,
            $resolve
              ? `<div> <div>42</div> </div>`
              : `<div> <div>foo-bar</div> </div>`,
            [],
            [],
            (ctx) => {
              const app = ctx.app;
              const s = app.$controller.scope;
              const bc = s.bindingContext;
              const oc = s.overrideContext;
              assert.strictEqual('data' in bc, true, 'data in bc');
              assert.strictEqual('err' in bc, true, 'err in bc');
              assert.strictEqual('data' in oc, false, 'data in oc');
              assert.strictEqual('err' in oc, false, 'err in oc');
            },
          );
        }
      }
      // #endregion
    }
  }
  for (const data of getTestData()) {
    (data.only ? $it.only : $it)(data.name,
      async function (ctx: PromiseTestExecutionContext<any>) {

        assert.strictEqual(ctx.error, null);
        const expectedContent = data.expectedInnerHtml;
        if (expectedContent !== null) {
          await ctx.platform.domQueue.yield();
          assert.html.innerEqual(ctx.host, expectedContent, 'innerHTML');
        }

        const expectedLog = data.expectedStartLog;
        if (expectedLog !== null) {
          ctx.assertCallSet(expectedLog, 'start lifecycle calls');
        }

        const additionalAssertions = data.additionalAssertions;
        if (additionalAssertions !== null) {
          await additionalAssertions(ctx);
        }
      },
      data);
  }

  class NegativeTestData implements TestSetupContext<App> {
    public readonly registrations: any[] = [];
    public readonly expectedStopLog: string[] = [];
    public readonly verifyStopCallsAsSet: boolean = false;
    public readonly promise: Promise<unknown> = Promise.resolve(42);
    public readonly delayPromise: DelayPromise = null;
    public readonly appType: Class<App> = App;
    public constructor(
      public readonly name: string,
      public readonly template: string,
    ) { }
  }

  function* getNegativeTestData() {
    yield new NegativeTestData(
      `pending cannot be used in isolation`,
      `<template><template pending>pending</template></template>`
    );
    yield new NegativeTestData(
      `then cannot be used in isolation`,
      `<template><template then>fulfilled</template></template>`
    );
    yield new NegativeTestData(
      `catch cannot be used in isolation`,
      `<template><template catch>rejected</template></template>`
    );
    yield new NegativeTestData(
      `pending cannot be nested inside an if.bind`,
      `<template><template if.bind="true"><template pending>pending</template></template></template>`
    );
    yield new NegativeTestData(
      `then cannot be nested inside an if.bind`,
      `<template><template if.bind="true"><template then>fulfilled</template></template></template>`
    );
    yield new NegativeTestData(
      `catch cannot be nested inside an if.bind`,
      `<template><template if.bind="true"><template catch>rejected</template></template></template>`
    );
    yield new NegativeTestData(
      `pending cannot be nested inside an else`,
      `<template><template if.bind="false"></template><template else><template pending>pending</template></template></template>`
    );
    yield new NegativeTestData(
      `then cannot be nested inside an else`,
      `<template><template if.bind="false"></template><template else><template then>fulfilled</template></template></template>`
    );
    yield new NegativeTestData(
      `catch cannot be nested inside an else`,
      `<template><template if.bind="false"></template><template else><template catch>rejected</template></template></template>`
    );
    yield new NegativeTestData(
      `pending cannot be nested inside a repeater`,
      `<template><template repeat.for="i of 1"><template pending>pending</template></template></template>`
    );
    yield new NegativeTestData(
      `then cannot be nested inside a repeater`,
      `<template><template repeat.for="i of 1"><template then>fulfilled</template></template></template>`
    );
    yield new NegativeTestData(
      `catch cannot be nested inside a repeater`,
      `<template><template repeat.for="i of 1"><template catch>rejected</template></template></template>`
    );
  }

  for (const data of getNegativeTestData()) {
    $it(data.name, async function (ctx: PromiseTestExecutionContext) {
      // assert.match(ctx.error.message, /The parent promise\.resolve not found; only `\*\[promise\.resolve\] > \*\[pending\|then\|catch\]` relation is supported\./);
      assert.match(ctx.error.message, /AUR0813/);
    }, data);
  }
});
