import 'mocha';
import { expect } from 'chai';
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler';
import { IExpressionParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/IExpressionParser';
import { ExpressionStub } from '@tests/unit/stubs/ExpressionStub';
import { ExpressionParserStub } from '@tests/unit/stubs/ExpressionParserStub';
import { FunctionCallArgumentCollectionStub } from '@tests/unit/stubs/FunctionCallArgumentCollectionStub';

describe('ExpressionsCompiler', () => {
    describe('compileExpressions', () => {
        describe('combines expressions as expected', () => {
            // arrange
            const code = 'part1 {{ a }} part2 {{ b }} part3';
            const testCases = [
                {
                    name: 'with ordered expressions',
                    expressions: [
                        new ExpressionStub().withPosition(6, 13).withEvaluatedResult('a'),
                        new ExpressionStub().withPosition(20, 27).withEvaluatedResult('b'),
                    ],
                    expected: 'part1 a part2 b part3',
                },
                {
                    name: 'unordered expressions',
                    expressions: [
                        new ExpressionStub().withPosition(20, 27).withEvaluatedResult('b'),
                        new ExpressionStub().withPosition(6, 13).withEvaluatedResult('a'),
                    ],
                    expected: 'part1 a part2 b part3',
                },
                {
                    name: 'with an optional expected argument that is not provided',
                    expressions: [
                        new ExpressionStub().withPosition(6, 13).withEvaluatedResult('a')
                            .withParameterNames(['optionalParameter'], true),
                        new ExpressionStub().withPosition(20, 27).withEvaluatedResult('b')
                            .withParameterNames(['optionalParameterTwo'], true),
                    ],
                    expected: 'part1 a part2 b part3',
                },
                {
                    name: 'with no expressions',
                    expressions: [],
                    expected: code,
                },
            ];
            for (const testCase of testCases) {
                it(testCase.name, () => {
                    const expressionParserMock = new ExpressionParserStub()
                        .withResult(testCase.expressions);
                    const args = new FunctionCallArgumentCollectionStub();
                    const sut = new MockableExpressionsCompiler(expressionParserMock);
                    // act
                    const actual = sut.compileExpressions(code, args);
                    // assert
                    expect(actual).to.equal(testCase.expected);
                });
            }
        });
        describe('arguments', () => {
            it('passes arguments to expressions as expected', () => {
                // arrange
                const expected = new FunctionCallArgumentCollectionStub()
                    .withArgument('test-arg', 'test-value');
                const code = 'non-important';
                const expressions = [
                    new ExpressionStub(),
                    new ExpressionStub(),
                ];
                const expressionParserMock = new ExpressionParserStub()
                    .withResult(expressions);
                const sut = new MockableExpressionsCompiler(expressionParserMock);
                // act
                sut.compileExpressions(code, expected);
                // assert
                expect(expressions[0].callHistory).to.have.lengthOf(1);
                expect(expressions[0].callHistory[0]).to.equal(expected);
                expect(expressions[1].callHistory).to.have.lengthOf(1);
                expect(expressions[1].callHistory[0]).to.equal(expected);
            });
            it('throws if arguments is undefined', () => {
                // arrange
                const expectedError = 'undefined args, send empty collection instead';
                const args = undefined;
                const expressionParserMock = new ExpressionParserStub();
                const sut = new MockableExpressionsCompiler(expressionParserMock);
                // act
                const act = () => sut.compileExpressions('code', args);
                // assert
                expect(act).to.throw(expectedError);
            });
        });
        describe('throws when expected argument is not provided but used in code', () => {
            // arrange
            const testCases = [
                {
                    name: 'empty parameters',
                    expressions: [
                        new ExpressionStub().withParameterNames(['parameter'], false),
                    ],
                    args: new FunctionCallArgumentCollectionStub(),
                    expectedError: 'parameter value(s) not provided for: "parameter" but used in code',
                },
                {
                    name: 'unnecessary parameter is provided',
                    expressions: [
                        new ExpressionStub().withParameterNames(['parameter'], false),
                    ],
                    args: new FunctionCallArgumentCollectionStub()
                        .withArgument('unnecessaryParameter', 'unnecessaryValue'),
                    expectedError: 'parameter value(s) not provided for: "parameter" but used in code',
                },
                {
                    name: 'multiple values are not provided',
                    expressions: [
                        new ExpressionStub().withParameterNames(['parameter1'], false),
                        new ExpressionStub().withParameterNames(['parameter2', 'parameter3'], false),
                    ],
                    args: new FunctionCallArgumentCollectionStub(),
                    expectedError: 'parameter value(s) not provided for: "parameter1", "parameter2", "parameter3" but used in code',
                },
                {
                    name: 'some values are provided',
                    expressions: [
                        new ExpressionStub().withParameterNames(['parameter1'], false),
                        new ExpressionStub().withParameterNames(['parameter2', 'parameter3'], false),
                    ],
                    args: new FunctionCallArgumentCollectionStub()
                        .withArgument('parameter2', 'value'),
                    expectedError: 'parameter value(s) not provided for: "parameter1", "parameter3" but used in code',
                },
            ];
            for (const testCase of testCases) {
                it(testCase.name, () => {
                    const code = 'non-important-code';
                    const expressionParserMock = new ExpressionParserStub()
                        .withResult(testCase.expressions);
                    const sut = new MockableExpressionsCompiler(expressionParserMock);
                    // act
                    const act = () => sut.compileExpressions(code, testCase.args);
                    // assert
                    expect(act).to.throw(testCase.expectedError);
                });
            }
        });
        it('calls parser with expected code', () => {
            // arrange
            const expected = 'expected-code';
            const expressionParserMock = new ExpressionParserStub();
            const sut = new MockableExpressionsCompiler(expressionParserMock);
            const args = new FunctionCallArgumentCollectionStub();
            // act
            sut.compileExpressions(expected, args);
            // assert
            expect(expressionParserMock.callHistory).to.have.lengthOf(1);
            expect(expressionParserMock.callHistory[0]).to.equal(expected);
        });
    });
});

class MockableExpressionsCompiler extends ExpressionsCompiler {
    constructor(extractor: IExpressionParser) {
        super(extractor);
    }
}
