'use strict';

const { expect } = require('chai');

const ServerlessError = require('../../../../../lib/serverless-error');
const parse = require('../../../../../lib/configuration/variables/parse');

describe('test/unit/lib/configuration/variables/parse.test.js', () => {
  describe('Valid', () => {
    it('should support partially variable value at begin of a string', () =>
      expect(parse('${type:address}foo')).to.deep.equal([
        { start: 0, end: 15, sources: [{ type: 'type', address: { value: 'address' } }] },
      ]));

    it('should support partially variable value at end of a string', () =>
      expect(parse('foo${type:address}')).to.deep.equal([
        { start: 3, end: 18, sources: [{ type: 'type', address: { value: 'address' } }] },
      ]));

    it('should support partially variable value in a middle of a string', () =>
      expect(parse('foo${type:address}bar')).to.deep.equal([
        { start: 3, end: 18, sources: [{ type: 'type', address: { value: 'address' } }] },
      ]));

    // ${type:}
    it('should support type only notation', () =>
      expect(parse('${type:}')).to.deep.equal([{ sources: [{ type: 'type' }] }]));

    // ${type:address}
    it('should support type and address', () =>
      expect(parse('${type:address}')).to.deep.equal([
        { sources: [{ type: 'type', address: { value: 'address' } }] },
      ]));

    // ${type(param)}
    it('should support param', () =>
      expect(parse('${type(param)}')).to.deep.equal([
        { sources: [{ type: 'type', params: [{ value: 'param' }] }] },
      ]));

    // ${type(param1, param2)}
    it('should support multiple params', () =>
      expect(parse('${type(param1, param2)}')).to.deep.equal([
        { sources: [{ type: 'type', params: [{ value: 'param1' }, { value: 'param2' }] }] },
      ]));

    // ${type(param1, ",},${\"param2", param3)}
    it('should support double quoted params', () =>
      expect(parse('${type(param1, ",},${\\"param2", param3)}')).to.deep.equal([
        {
          sources: [
            {
              type: 'type',
              params: [{ value: 'param1' }, { value: ',},${"param2' }, { value: 'param3' }],
            },
          ],
        },
      ]));

    // ${type(param1, ',},${\'param2' )}
    it('should support single quoted params', () =>
      expect(parse("${type(param1, ',},${param2' )}")).to.deep.equal([
        {
          sources: [
            {
              type: 'type',
              params: [{ value: 'param1' }, { value: ',},${param2' }],
            },
          ],
        },
      ]));

    // ${type(param1, 232, param3)}
    it('should support number params', () =>
      expect(parse('${type(param1, 232, param3)}')).to.deep.equal([
        {
          sources: [
            {
              type: 'type',
              params: [{ value: 'param1' }, { value: 232 }, { value: 'param3' }],
            },
          ],
        },
      ]));

    // ${type(param1, true, param3)}
    it('should support boolean params', () =>
      expect(parse('${type(param1, true, param3)}')).to.deep.equal([
        {
          sources: [
            {
              type: 'type',
              params: [{ value: 'param1' }, { value: true }, { value: 'param3' }],
            },
          ],
        },
      ]));

    // ${type(param1, null, param3) }
    it('should support null params', () =>
      expect(parse('${type(param1, null, param3) }')).to.deep.equal([
        {
          sources: [
            {
              type: 'type',
              params: [{ value: 'param1' }, { value: null }, { value: 'param3' }],
            },
          ],
        },
      ]));

    // ${type(param):address}
    it('should support param and address', () =>
      expect(parse('${type(param):address}')).to.deep.equal([
        {
          sources: [{ type: 'type', params: [{ value: 'param' }], address: { value: 'address' } }],
        },
      ]));

    // ${type(param):",},${\"address"}
    it('should support quoted address', () =>
      expect(parse('${type(param):",},${\\"address"}')).to.deep.equal([
        {
          sources: [
            { type: 'type', params: [{ value: 'param' }], address: { value: ',},${"address' } },
          ],
        },
      ]));

    // ${type(param):'}$address' }
    it('should support single quoted address', () =>
      expect(parse("${type(param):'}$address' }")).to.deep.equal([
        {
          sources: [
            { type: 'type', params: [{ value: 'param' }], address: { value: '}$address' } },
          ],
        },
      ]));

    // ${type1(param1), type2(param2):address2, type3:, type4:address4}
    it('should support fallback sources', () =>
      expect(
        parse('${type1(param1), type2(param2):"address2", type3:, type4:address4}')
      ).to.deep.equal([
        {
          sources: [
            { type: 'type1', params: [{ value: 'param1' }] },
            { type: 'type2', params: [{ value: 'param2' }], address: { value: 'address2' } },
            { type: 'type3' },
            { type: 'type4', address: { value: 'address4' } },
          ],
        },
      ]));

    // ${type(param), "foo, bar"}
    it('should support double quoted string as fallback source', () =>
      expect(parse('${type(param), "foo, bar"}')).to.deep.equal([
        {
          sources: [{ type: 'type', params: [{ value: 'param' }] }, { value: 'foo, bar' }],
        },
      ]));

    // ${type(param), 'foo, bar' }
    it('should support single quoted string as fallback source', () => {
      expect(parse("${type(param), 'foo, bar' }")).to.deep.equal([
        {
          sources: [{ type: 'type', params: [{ value: 'param' }] }, { value: 'foo, bar' }],
        },
      ]);
    });

    // ${type(param), 232}
    it('should support number as a fallback source', () =>
      expect(parse('${type(param), 232}')).to.deep.equal([
        {
          sources: [{ type: 'type', params: [{ value: 'param' }] }, { value: 232 }],
        },
      ]));

    // ${type(param), true}
    it('should support bolean "true" as a fallback source', () =>
      expect(parse('${type(param), true}')).to.deep.equal([
        {
          sources: [{ type: 'type', params: [{ value: 'param' }] }, { value: true }],
        },
      ]));

    // ${type(param), false }
    it('should support bolean "false" as a fallback source', () =>
      expect(parse('${type(param), false }')).to.deep.equal([
        {
          sources: [{ type: 'type', params: [{ value: 'param' }] }, { value: false }],
        },
      ]));

    // ${type(param), null}
    it('should support null as a fallback source', () =>
      expect(parse('${type(param), null}')).to.deep.equal([
        {
          sources: [{ type: 'type', params: [{ value: 'param' }] }, { value: null }],
        },
      ]));

    // ${type()}
    it('should support empty parens', () =>
      expect(parse('${type()}')).to.deep.equal([
        {
          sources: [{ type: 'type', params: [] }],
        },
      ]));

    // ${type(\t)}
    it('should ignore any whitespace between brackets', () =>
      expect(parse('${type(\t)}')).to.deep.equal([{ sources: [{ type: 'type', params: [] }] }]));

    // ${type(,,)}
    it('should support skipping arguments', () =>
      expect(parse('${type(,,)}')).to.deep.equal([
        {
          sources: [{ type: 'type', params: [{ value: null }, { value: null }] }],
        },
      ]));

    // ${type(${innerType(innerParam, ${deep:}):innerAddress}, foo${bar:}): address}
    it('should support variables in params', () =>
      expect(
        parse('${type(${innerType(innerParam, ${deep:}):innerAddress}, foo${bar:}): address}')
      ).to.deep.equal([
        {
          sources: [
            {
              type: 'type',
              params: [
                {
                  value: '${innerType(innerParam, ${deep:}):innerAddress}',
                  variables: [
                    {
                      sources: [
                        {
                          type: 'innerType',
                          params: [
                            { value: 'innerParam' },
                            { value: '${deep:}', variables: [{ sources: [{ type: 'deep' }] }] },
                          ],
                          address: { value: 'innerAddress' },
                        },
                      ],
                    },
                  ],
                },
                {
                  value: 'foo${bar:}',
                  variables: [
                    {
                      start: 3,
                      end: 10,
                      sources: [{ type: 'bar' }],
                    },
                  ],
                },
              ],
              address: { value: 'address' },
            },
          ],
        },
      ]));

    // ${type(params):${innerType(innerParam):innerAddress}}
    // ${type(param):foo${innerType(innerParam)}}
    it('should support variables in address', () => {
      expect(parse('${type(param):${innerType(innerParam):innerAddress}}')).to.deep.equal([
        {
          sources: [
            {
              type: 'type',
              params: [{ value: 'param' }],
              address: {
                value: '${innerType(innerParam):innerAddress}',
                variables: [
                  {
                    sources: [
                      {
                        type: 'innerType',
                        params: [{ value: 'innerParam' }],
                        address: { value: 'innerAddress' },
                      },
                    ],
                  },
                ],
              },
            },
          ],
        },
      ]);
      expect(parse('${type(param):foo${innerType(innerParam)}}')).to.deep.equal([
        {
          sources: [
            {
              type: 'type',
              params: [{ value: 'param' }],
              address: {
                value: 'foo${innerType(innerParam)}',
                variables: [
                  {
                    start: 3,
                    end: 27,
                    sources: [
                      {
                        type: 'innerType',
                        params: [{ value: 'innerParam' }],
                      },
                    ],
                  },
                ],
              },
            },
          ],
        },
      ]);
    });

    // ${type.dot(param)}
    it('should support dots in type notation', () =>
      expect(parse('${type.dot(param)}')).to.deep.equal([
        { sources: [{ type: 'type.dot', params: [{ value: 'param' }] }] },
      ]));

    // ${type.us-east-1(param)}
    it('should support hyphens in type notation', () =>
      expect(parse('${type.us-east-1(param)}')).to.deep.equal([
        { sources: [{ type: 'type.us-east-1', params: [{ value: 'param' }] }] },
      ]));

    // ${AWS::${type:address}}
    it('should support variable nested in foreign variable', () =>
      expect(parse('${AWS::${type:address}}')).to.deep.equal([
        { start: 7, end: 22, sources: [{ type: 'type', address: { value: 'address' } }] },
      ]));

    // ${sourceDirect:}elo${sourceIncomplete:}
    it('should support multiple variables in a value', () =>
      expect(parse('${type1:}elo${type2:}')).to.deep.equal([
        { start: 0, end: 9, sources: [{ type: 'type1' }] },
        { start: 12, end: 21, sources: [{ type: 'type2' }] },
      ]));

    // ${s:${s:}, 1}
    // https://github.com/serverless/serverless/issues/8999
    it("should recognize variables in address, if it's followed by source", () =>
      expect(parse('${s:${s:}, 1}')).to.deep.equal([
        {
          sources: [
            {
              type: 's',
              address: {
                value: '${s:}',
                variables: [{ sources: [{ type: 's' }] }],
              },
            },
            { value: 1 },
          ],
        },
      ]));

    // ${s:, s:${s:}}
    // https://github.com/serverless/serverless/issues/9010
    it('should resolve nested sources, when at least one parent source was resolved', () =>
      expect(parse('${s:, s:${s:}}')).to.deep.equal([
        {
          sources: [
            { type: 's' },
            {
              type: 's',
              address: {
                value: '${s:}',
                variables: [{ sources: [{ type: 's' }] }],
              },
            },
          ],
        },
      ]));
  });

  describe('Invalid', () => {
    // ${type(${invalid.notation}):address}
    it('should reject invalid configuration in params', () =>
      expect(() => parse('${type(${invalid.notation}):address}'))
        .to.throw(ServerlessError)
        .with.property('code', 'INVALID_VARIABLE_TYPE'));

    // ${type(params):${innerType(innerParam):${sdfs.fefef}
    it('should reject invalid configuration in address', () =>
      expect(() => parse('${type(params):${innerType(innerParam):${sdfs.fefef}'))
        .to.throw(ServerlessError)
        .with.property('code', 'INVALID_VARIABLE_TYPE'));

    // ${type:foo:bar}
    it('should reject ":" strings in address literal', () =>
      expect(() => parse('${type:foo:bar}'))
        .to.throw(ServerlessError)
        .with.property('code', 'INVALID_VARIABLE_ADDRESS'));

    // ${type:address
    it('should detect not closed variable', () => {
      expect(() => parse('${type:address'))
        .to.throw(ServerlessError)
        .with.property('code', 'UNTERMINATED_VARIABLE');
      expect(() => parse('${type(foo)'))
        .to.throw(ServerlessError)
        .with.property('code', 'UNTERMINATED_VARIABLE');
      expect(() => parse('${s:, s:${s:}'))
        .to.throw(ServerlessError)
        .with.property('code', 'UNTERMINATED_VARIABLE');
      expect(() => parse('${s:, s:${s:'))
        .to.throw(ServerlessError)
        .with.property('code', 'UNTERMINATED_VARIABLE');
    });

    // ${type("\u")}
    it('should reject invalid string literal', () =>
      expect(() => parse('${type("\\u")}'))
        .to.throw(ServerlessError)
        .with.property('code', 'INVALID_VARIABLE_STRING_LITERAL'));

    // '${type:foo, elo}
    it('should reject invalid source literal', () =>
      expect(() => parse('${type:foo, elo}'))
        .to.throw(ServerlessError)
        .with.property('code', 'INVALID_VARIABLE_LITERAL_SOURCE'));

    // ${type('foo')bar}
    it('should reject missing colon for address', () =>
      expect(() => parse("${type('foo')bar}"))
        .to.throw(ServerlessError)
        .with.property('code', 'INVALID_VARIABLE_ADDRESS'));

    // ${type:"address"marko}
    it('should reject invalid address configuration', () =>
      expect(() => parse('${type:"address"marko}'))
        .to.throw(ServerlessError)
        .with.property('code', 'INVALID_VARIABLE_ADDRESS'));

    // ${type:foo, ---}
    // ${type:foo, 000:}
    // ${type:foo, 000()}
    // ${type:foo, aa--}
    it('should reject invalid following source', () => {
      expect(() => parse('${type:foo, ___}'))
        .to.throw(ServerlessError)
        .with.property('code', 'INVALID_VARIABLE_SOURCE');

      expect(() => parse('${type:foo, --}'))
        .to.throw(ServerlessError)
        .with.property('code', 'INVALID_VARIABLE_LITERAL_SOURCE');

      expect(() => parse('${type:foo, 000:}'))
        .to.throw(ServerlessError)
        .with.property('code', 'INVALID_VARIABLE_SOURCE');

      expect(() => parse('${type:foo, 000()}'))
        .to.throw(ServerlessError)
        .with.property('code', 'INVALID_VARIABLE_SOURCE');

      expect(() => parse('${type:foo, aa--}'))
        .to.throw(ServerlessError)
        .with.property('code', 'INVALID_VARIABLE_LITERAL_SOURCE');

      expect(() => parse('${type:foo, aa__}'))
        .to.throw(ServerlessError)
        .with.property('code', 'INVALID_VARIABLE_SOURCE');

      expect(() => parse('${type:foo,"dev",20}'))
        .to.throw(ServerlessError)
        .with.property('code', 'INVALID_VARIABLE_SOURCE');
    });

    // ${type(${AWS::Region})}
    // ${type(${foo::Region})}
    it('should reject nested foreign variables', () => {
      expect(() => parse('${type(${AWS::Region})}'))
        .to.throw(ServerlessError)
        .with.property('code', 'INVALID_VARIABLE_TYPE');

      expect(() => parse('${type(${foo::Region})}'))
        .to.throw(ServerlessError)
        .with.property('code', 'INVALID_VARIABLE_ADDRESS');
    });

    // ${type(foo})
    // ${type(foo,})
    it('should reject closing bracket at unexpected location', () => {
      expect(() => parse('${type(foo})'))
        .to.throw(ServerlessError)
        .with.property('code', 'INVALID_VARIABLE_PARAM');
      expect(() => parse('${type(foo,})'))
        .to.throw(ServerlessError)
        .with.property('code', 'INVALID_VARIABLE_PARAM');
    });

    // ${type('foo'bar)}
    it('should reject unexpected content after param string', () =>
      expect(() => parse("${type('foo'bar)}"))
        .to.throw(ServerlessError)
        .with.property('code', 'INVALID_VARIABLE_PARAM'));
  });

  describe('Foreign', () => {
    // ${}
    it('should ignore empty value', () => expect(parse('${}')).to.equal(null));

    // ${${AWS::Region}}
    it('should ignore nested foreign notations', () =>
      expect(parse('${${AWS::Region}}')).to.equal(null));

    // ${type}
    it('should ignore just type name string', () => expect(parse('${type}')).to.equal(null));

    // ${AWS::Region}
    // ${foo::Region}
    it('should ignore double clon vars ', () => {
      expect(parse('${AWS::Region}')).to.equal(null);
      expect(parse('${foo::Region}')).to.equal(null);
    });

    // ${Database}
    // ${stageVariables.stageName}
    it('should ignore AWS CF references', () => {
      expect(parse('${Database}')).to.equal(null);
      expect(parse('foo ${stageVariables.stageName} var')).to.equal(null);
    });

    //  ${sour${inner.type}ce}
    it('should ignore nested not supported notations', () =>
      expect(parse('fo${bla${foo}}o  ${so,ur${inner.type}ce} var')).to.equal(null));
  });

  describe('Not used', () => {
    // foo bar ()
    it('should return null', () => expect(parse('fo$o b$$ar ()')).to.equal(null));

    // foo\${elo:}
    it('should support escape character', () =>
      expect(parse('e\\${s:}n\\$${s:}qe\\\\\\${s:}qn\\\\${s:}')).to.deep.equal([
        {
          start: 1,
          end: 3,
          value: '$',
        },
        {
          start: 10,
          end: 15,
          sources: [{ type: 's' }],
        },
        {
          start: 17,
          end: 21,
          value: '\\$',
        },
        {
          start: 27,
          end: 29,
          value: '\\',
        },
        {
          start: 29,
          end: 34,
          sources: [{ type: 's' }],
        },
      ]));
  });
});
