/**
 * @license
 * Copyright 2020 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import {nameUsedWithConflictingParam} from '../../../build/src/core/variables.js';
import {assert} from '../../../node_modules/chai/index.js';
import {
  MockParameterModelWithVar,
  MockProcedureModel,
} from '../test_helpers/procedures.js';
import {
  sharedTestSetup,
  sharedTestTeardown,
} from '../test_helpers/setup_teardown.js';

suite('Variables', function () {
  setup(function () {
    sharedTestSetup.call(this);
    this.workspace = new Blockly.Workspace();
    Blockly.defineBlocksWithJsonArray([
      {
        'type': 'get_var_block',
        'message0': '%1',
        'args0': [
          {
            'type': 'field_variable',
            'name': 'VAR',
            'variableTypes': ['', 'type1', 'type2'],
          },
        ],
        'output': null,
      },
      // Block for variable setter.
      {
        'type': 'set_var_block',
        'message0': '%{BKY_VARIABLES_SET}',
        'args0': [
          {
            'type': 'field_variable',
            'name': 'VAR',
            'variableTypes': ['', 'type1', 'type2'],
          },
          {
            'type': 'input_value',
            'name': 'VALUE',
          },
        ],
        'previousStatement': null,
        'nextStatement': null,
      },
    ]);
    this.variableMap = this.workspace.getVariableMap();
    this.variableMap.createVariable('foo', 'type1', '1');
    this.variableMap.createVariable('bar', 'type1', '2');
    this.variableMap.createVariable('baz', 'type1', '3');
  });

  teardown(function () {
    sharedTestTeardown.call(this);
  });

  /**
   * Create a test get_var_block.
   * Will fail if get_var_block isn't defined.
   * @param {!Blockly.Workspace} workspace The workspace on which to create the
   *     block.
   * @param {!string} variableId The id of the variable to reference.
   * @return {!Blockly.Block} The created block.
   */
  function createTestVarBlock(workspace, variableId) {
    // Turn off events to avoid testing XML at the same time.
    Blockly.Events.disable();
    const block = new Blockly.Block(workspace, 'get_var_block');
    block.inputList[0].fieldRow[0].setValue(variableId);
    Blockly.Events.enable();
    return block;
  }

  test('can be deleted when two connected blocks reference the same variable', function () {
    const getter = new Blockly.Block(this.workspace, 'get_var_block');
    getter.getField('VAR').setValue('1');

    const setter = new Blockly.Block(this.workspace, 'set_var_block');
    setter.getField('VAR').setValue('1');
    setter.getInput('VALUE').connection.connect(getter.outputConnection);

    this.variableMap.deleteVariable(this.variableMap.getVariableById('1'));
    // Both blocks should have been deleted.
    assert.equal(0, this.workspace.getAllBlocks(false).length);
    // The variable itself should have been deleted.
    assert.equal(this.variableMap.getVariableById('1'), undefined);
  });

  suite('allUsedVarModels', function () {
    test('All used', function () {
      createTestVarBlock(this.workspace, '1');
      createTestVarBlock(this.workspace, '2');
      createTestVarBlock(this.workspace, '3');

      const result = Blockly.Variables.allUsedVarModels(this.workspace);
      assert.equal(
        result.length,
        3,
        'Expected three variables in the list of used variables',
      );
    });

    test('Some unused', function () {
      createTestVarBlock(this.workspace, '2');

      const result = Blockly.Variables.allUsedVarModels(this.workspace);
      assert.equal(
        result.length,
        1,
        'Expected one variable in the list of used variables',
      );
      assert.equal(
        result[0].getId(),
        '2',
        'Expected variable with ID 2 in the list of used variables',
      );
    });

    test('Var used twice', function () {
      createTestVarBlock(this.workspace, '2');
      createTestVarBlock(this.workspace, '2');

      const result = Blockly.Variables.allUsedVarModels(this.workspace);
      // Using the same variable multiple times should not change the number of
      // elements in the list.
      assert.equal(
        result.length,
        1,
        'Expected one variable in the list of used variables',
      );
      assert.equal(
        result[0].getId(),
        '2',
        'Expected variable with ID 2 in the list of used variables',
      );
    });

    test('All unused', function () {
      const result = Blockly.Variables.allUsedVarModels(this.workspace);
      assert.equal(
        result.length,
        0,
        'Expected no variables in the list of used variables',
      );
    });
  });
  suite('getVariable', function () {
    test('By ID', function () {
      const var1 = this.variableMap.createVariable('name1', 'type1', 'id1');
      const var2 = this.variableMap.createVariable('name2', 'type1', 'id2');
      const var3 = this.variableMap.createVariable('name3', 'type2', 'id3');
      const result1 = Blockly.Variables.getVariable(this.workspace, 'id1');
      const result2 = Blockly.Variables.getVariable(this.workspace, 'id2');
      const result3 = Blockly.Variables.getVariable(this.workspace, 'id3');

      assert.equal(var1, result1);
      assert.equal(var2, result2);
      assert.equal(var3, result3);
    });

    test('By name and type', function () {
      const var1 = this.variableMap.createVariable('name1', 'type1', 'id1');
      const var2 = this.variableMap.createVariable('name2', 'type1', 'id2');
      const var3 = this.variableMap.createVariable('name3', 'type2', 'id3');
      const result1 = Blockly.Variables.getVariable(
        this.workspace,
        null,
        'name1',
        'type1',
      );
      const result2 = Blockly.Variables.getVariable(
        this.workspace,
        null,
        'name2',
        'type1',
      );
      const result3 = Blockly.Variables.getVariable(
        this.workspace,
        null,
        'name3',
        'type2',
      );

      // Searching by name + type is correct.
      assert.equal(var1, result1);
      assert.equal(var2, result2);
      assert.equal(var3, result3);
    });

    test('Bad ID with name and type fallback', function () {
      const var1 = this.variableMap.createVariable('name1', 'type1', 'id1');
      const var2 = this.variableMap.createVariable('name2', 'type1', 'id2');
      const var3 = this.variableMap.createVariable('name3', 'type2', 'id3');
      const result1 = Blockly.Variables.getVariable(
        this.workspace,
        'badId',
        'name1',
        'type1',
      );
      const result2 = Blockly.Variables.getVariable(
        this.workspace,
        'badId',
        'name2',
        'type1',
      );
      const result3 = Blockly.Variables.getVariable(
        this.workspace,
        'badId',
        'name3',
        'type2',
      );

      // Searching by ID failed, but falling back onto name + type is correct.
      assert.equal(var1, result1);
      assert.equal(var2, result2);
      assert.equal(var3, result3);
    });
  });

  suite('renaming variables creating conflicts', function () {
    suite('renaming variables creating parameter conflicts', function () {
      test('conflicts within legacy procedure blocks return the procedure name', function () {
        Blockly.serialization.blocks.append(
          {
            'type': 'procedures_defnoreturn',
            'extraState': {
              'params': [
                {
                  'name': 'x',
                  'id': '6l3P%Y!9EgA(Nh{E`Tl,',
                },
                {
                  'name': 'y',
                  'id': 'l1EtlJe%z_M[O-@uPAQ8',
                },
              ],
            },
            'fields': {
              'NAME': 'test name',
            },
          },
          this.workspace,
        );

        assert.equal(
          'test name',
          nameUsedWithConflictingParam('x', 'y', this.workspace),
          'Expected the name of the procedure with the conflicting ' +
            'param to be returned',
        );
      });

      test(
        'if no legacy block has the old var name, no procedure ' +
          'name is returned',
        function () {
          Blockly.serialization.blocks.append(
            {
              'type': 'procedures_defnoreturn',
              'extraState': {
                'params': [
                  {
                    'name': 'definitely not x',
                    'id': '6l3P%Y!9EgA(Nh{E`Tl,',
                  },
                  {
                    'name': 'y',
                    'id': 'l1EtlJe%z_M[O-@uPAQ8',
                  },
                ],
              },
              'fields': {
                'NAME': 'test name',
              },
            },
            this.workspace,
          );

          assert.isNull(
            nameUsedWithConflictingParam('x', 'y', this.workspace),
            'Expected there to be no conflict',
          );
        },
      );

      test('conflicts within procedure models return the procedure name', function () {
        this.workspace
          .getProcedureMap()
          .add(
            new MockProcedureModel('test name')
              .insertParameter(
                new MockParameterModelWithVar('x', this.workspace),
                0,
              )
              .insertParameter(
                new MockParameterModelWithVar('y', this.workspace),
                0,
              ),
          );

        assert.equal(
          'test name',
          nameUsedWithConflictingParam('x', 'y', this.workspace),
          'Expected the name of the procedure with the conflicting ' +
            'param to be returned',
        );
      });

      test(
        'if no procedure model has the old var, no procedure ' +
          'name is returned',
        function () {
          this.workspace
            .getProcedureMap()
            .add(
              new MockProcedureModel('test name')
                .insertParameter(
                  new MockParameterModelWithVar(
                    'definitely not x',
                    this.workspace,
                  ),
                  0,
                )
                .insertParameter(
                  new MockParameterModelWithVar('y', this.workspace),
                  0,
                ),
            );

          assert.isNull(
            nameUsedWithConflictingParam('x', 'y', this.workspace),
            'Expected there to be no conflict',
          );
        },
      );
    });
  });
});
