import { set } from '@ember/object';
import { DEBUG } from '@glimmer/env';
import { RenderingTestCase, defineComponent, moduleFor, runTask } from 'internal-test-helpers';
import { Component } from '../../utils/helpers';

moduleFor(
  'Helpers test: {{fn}}',
  class extends RenderingTestCase {
    beforeEach() {
      this.registerHelper('invoke', function ([fn]) {
        return fn();
      });

      let testContext = this;
      this.registerComponent('stash', {
        ComponentClass: Component.extend({
          init() {
            this._super(...arguments);
            testContext.stashedFn = this.stashedFn;
          },
        }),
      });
    }

    '@test fn can be shadowed'() {
      let First = defineComponent(
        { fn: boundFn, boundFn, id, invoke },
        `[{{invoke (fn id 1)}}]{{#let boundFn as |fn|}}[{{invoke (fn id 2)}}{{/let}}]`
      );
      this.renderComponent(First, { expect: `[bound:1][bound:2]` });
    }

    '@test updates when arguments change'() {
      this.render(`{{invoke (fn this.myFunc this.arg1 this.arg2)}}`, {
        myFunc(arg1, arg2) {
          return `arg1: ${arg1}, arg2: ${arg2}`;
        },

        arg1: 'foo',
        arg2: 'bar',
      });

      this.assertText('arg1: foo, arg2: bar');

      this.assertStableRerender();

      runTask(() => set(this.context, 'arg1', 'qux'));
      this.assertText('arg1: qux, arg2: bar');

      runTask(() => set(this.context, 'arg2', 'derp'));
      this.assertText('arg1: qux, arg2: derp');

      runTask(() => {
        set(this.context, 'arg1', 'foo');
        set(this.context, 'arg2', 'bar');
      });

      this.assertText('arg1: foo, arg2: bar');
    }

    '@test updates when the function changes'() {
      let func1 = (arg1, arg2) => `arg1: ${arg1}, arg2: ${arg2}`;
      let func2 = (arg1, arg2) => `arg2: ${arg2}, arg1: ${arg1}`;

      this.render(`{{invoke (fn this.myFunc this.arg1 this.arg2)}}`, {
        myFunc: func1,

        arg1: 'foo',
        arg2: 'bar',
      });

      this.assertText('arg1: foo, arg2: bar');
      this.assertStableRerender();

      runTask(() => set(this.context, 'myFunc', func2));
      this.assertText('arg2: bar, arg1: foo');

      runTask(() => set(this.context, 'myFunc', func1));
      this.assertText('arg1: foo, arg2: bar');
    }

    '@test a stashed fn result update arguments when invoked'(assert) {
      this.render(`{{stash stashedFn=(fn this.myFunc this.arg1 this.arg2)}}`, {
        myFunc(arg1, arg2) {
          return `arg1: ${arg1}, arg2: ${arg2}`;
        },

        arg1: 'foo',
        arg2: 'bar',
      });

      assert.equal(this.stashedFn(), 'arg1: foo, arg2: bar');

      runTask(() => set(this.context, 'arg1', 'qux'));
      assert.equal(this.stashedFn(), 'arg1: qux, arg2: bar');

      runTask(() => set(this.context, 'arg2', 'derp'));
      assert.equal(this.stashedFn(), 'arg1: qux, arg2: derp');

      runTask(() => {
        set(this.context, 'arg1', 'foo');
        set(this.context, 'arg2', 'bar');
      });

      assert.equal(this.stashedFn(), 'arg1: foo, arg2: bar');
    }

    '@test a stashed fn result invokes the correct function when the bound function changes'(
      assert
    ) {
      let func1 = (arg1, arg2) => `arg1: ${arg1}, arg2: ${arg2}`;
      let func2 = (arg1, arg2) => `arg2: ${arg2}, arg1: ${arg1}`;

      this.render(`{{stash stashedFn=(fn this.myFunc this.arg1 this.arg2)}}`, {
        myFunc: func1,

        arg1: 'foo',
        arg2: 'bar',
      });

      assert.equal(this.stashedFn(), 'arg1: foo, arg2: bar');

      runTask(() => set(this.context, 'myFunc', func2));
      assert.equal(this.stashedFn(), 'arg2: bar, arg1: foo');

      runTask(() => set(this.context, 'myFunc', func1));
      assert.equal(this.stashedFn(), 'arg1: foo, arg2: bar');
    }

    '@test there is no `this` context within the callback'(assert) {
      if (DEBUG) {
        assert.expect(0);
        return;
      }

      this.render(`{{stash stashedFn=(fn this.myFunc this.arg1)}}`, {
        myFunc() {
          assert.strictEqual(this, null, 'this is bound to null in production builds');
        },
      });

      this.stashedFn();
    }

    '@test can use `this` if bound prior to passing to fn'(assert) {
      this.render(`{{stash stashedFn=(fn this.myFunc2 this.arg1)}}`, {
        myFunc(arg1) {
          return `arg1: ${arg1}, arg2: ${this.arg2}`;
        },
        get myFunc2() {
          return this.myFunc.bind(this);
        },

        arg1: 'foo',
        arg2: 'bar',
      });

      assert.equal(this.stashedFn(), 'arg1: foo, arg2: bar');
    }

    '@test partially applies each layer when nested [GH#17959]'() {
      this.render(`{{invoke (fn (fn (fn this.myFunc this.arg1) this.arg2) this.arg3)}}`, {
        myFunc(arg1, arg2, arg3) {
          return `arg1: ${arg1}, arg2: ${arg2}, arg3: ${arg3}`;
        },

        arg1: 'foo',
        arg2: 'bar',
        arg3: 'qux',
      });

      this.assertText('arg1: foo, arg2: bar, arg3: qux');
      this.assertStableRerender();

      runTask(() => set(this.context, 'arg1', 'qux'));
      this.assertText('arg1: qux, arg2: bar, arg3: qux');

      runTask(() => set(this.context, 'arg2', 'derp'));
      this.assertText('arg1: qux, arg2: derp, arg3: qux');

      runTask(() => set(this.context, 'arg3', 'huzzah'));
      this.assertText('arg1: qux, arg2: derp, arg3: huzzah');

      runTask(() => {
        set(this.context, 'arg1', 'foo');
        set(this.context, 'arg2', 'bar');
        set(this.context, 'arg3', 'qux');
      });

      this.assertText('arg1: foo, arg2: bar, arg3: qux');
    }

    '@test can be used on the result of `mut`'() {
      this.render(`{{this.arg1}}{{stash stashedFn=(fn (mut this.arg1) this.arg2)}}`, {
        arg1: 'foo',
        arg2: 'bar',
      });

      this.assertText('foo');

      runTask(() => this.stashedFn());

      this.assertText('bar');
    }

    '@test can be used on the result of `mut` with a falsy value'() {
      this.render(`{{this.arg1}}{{stash stashedFn=(fn (mut this.arg1) this.arg2)}}`, {
        arg1: 'foo',
        arg2: false,
      });

      this.assertText('foo');

      runTask(() => this.stashedFn());

      this.assertText('false');
    }
  }
);

function invoke(fn) {
  return fn();
}

function boundFn(fn, ...args) {
  return () => fn(...args.map((arg) => `bound:${arg}`));
}

let id = (arg) => arg;
