import 'mocha';
import { expect } from 'chai';
import { ExpressionPosition } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPosition';
import { ExpressionEvaluator } from '@/application/Parser/Script/Compiler/Expressions/Expression/Expression';
import { Expression } from '@/application/Parser/Script/Compiler/Expressions/Expression/Expression';
import { IReadOnlyFunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/IFunctionParameterCollection';
import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/FunctionCall/Argument/IFunctionCallArgumentCollection';
import { FunctionCallArgumentCollectionStub } from '@tests/unit/stubs/FunctionCallArgumentCollectionStub';
import { FunctionParameterCollectionStub } from '@tests/unit/stubs/FunctionParameterCollectionStub';
import { FunctionCallArgumentStub } from '@tests/unit/stubs/FunctionCallArgumentStub';

describe('Expression', () => {
    describe('ctor', () => {
        describe('position', () => {
            it('throws if undefined', () => {
                // arrange
                const expectedError = 'undefined position';
                const position = undefined;
                // act
                const act = () => new ExpressionBuilder()
                    .withPosition(position)
                    .build();
                // assert
                expect(act).to.throw(expectedError);
            });
            it('sets as expected', () => {
                // arrange
                const expected = new ExpressionPosition(0, 5);
                // act
                const actual = new ExpressionBuilder()
                    .withPosition(expected)
                    .build();
                // assert
                expect(actual.position).to.equal(expected);
            });
        });
        describe('parameters', () => {
            it('defaults to empty array if undefined', () => {
                // arrange
                const parameters = undefined;
                // act
                const actual = new ExpressionBuilder()
                    .withParameters(parameters)
                    .build();
                // assert
                expect(actual.parameters);
                expect(actual.parameters.all);
                expect(actual.parameters.all.length).to.equal(0);
            });
            it('sets as expected', () => {
                // arrange
                const expected = new FunctionParameterCollectionStub()
                    .withParameterName('firstParameterName')
                    .withParameterName('secondParameterName');
                // act
                const actual = new ExpressionBuilder()
                    .withParameters(expected)
                    .build();
                // assert
                expect(actual.parameters).to.deep.equal(expected);
            });
        });
        describe('evaluator', () => {
            it('throws if undefined', () => {
                // arrange
                const expectedError = 'undefined evaluator';
                const evaluator = undefined;
                // act
                const act = () => new ExpressionBuilder()
                    .withEvaluator(evaluator)
                    .build();
                // assert
                expect(act).to.throw(expectedError);
            });
        });
    });
    describe('evaluate', () => {
        describe('throws with invalid arguments', () => {
            const testCases = [
                {
                    name: 'throws if arguments is undefined',
                    args: undefined,
                    expectedError: 'undefined args, send empty collection instead',
                },
                {
                    name: 'throws when some of the required args are not provided',
                    sut: (i: ExpressionBuilder) => i.withParameterNames(['a', 'b', 'c'], false),
                    args: new FunctionCallArgumentCollectionStub().withArgument('b', 'provided'),
                    expectedError: 'argument values are provided for required parameters: "a", "c"',
                },
                {
                    name: 'throws when none of the required args are not provided',
                    sut: (i: ExpressionBuilder) => i.withParameterNames(['a', 'b'], false),
                    args: new FunctionCallArgumentCollectionStub().withArgument('c', 'unrelated'),
                    expectedError: 'argument values are provided for required parameters: "a", "b"',
                },
            ];
            for (const testCase of testCases) {
                it(testCase.name, () => {
                    // arrange
                    const sutBuilder = new ExpressionBuilder();
                    if (testCase.sut) {
                        testCase.sut(sutBuilder);
                    }
                    const sut = sutBuilder.build();
                    // act
                    const act = () => sut.evaluate(testCase.args);
                    // assert
                    expect(act).to.throw(testCase.expectedError);
                });
            }
        });
        it('returns result from evaluator', () => {
            // arrange
            const evaluatorMock: ExpressionEvaluator = (args) =>
                `"${args
                    .getAllParameterNames()
                    .map((name) => args.getArgument(name))
                    .map((arg) => `${arg.parameterName}': '${arg.argumentValue}'`)
                    .join('", "')}"`;
            const givenArguments = new FunctionCallArgumentCollectionStub()
                .withArgument('parameter1', 'value1')
                .withArgument('parameter2', 'value2');
            const expectedParameterNames = givenArguments.getAllParameterNames();
            const expected = evaluatorMock(givenArguments);
            const sut = new ExpressionBuilder()
                .withEvaluator(evaluatorMock)
                .withParameterNames(expectedParameterNames)
                .build();
            // arrange
            const actual = sut.evaluate(givenArguments);
            // assert
            expect(expected).to.equal(actual,
                `\nGiven arguments: ${JSON.stringify(givenArguments)}\n` +
                `\nExpected parameter names: ${JSON.stringify(expectedParameterNames)}\n`,
            );
        });
        describe('filters unused parameters', () => {
            // arrange
            const testCases = [
                {
                    name: 'with a provided argument',
                    expressionParameters: new FunctionParameterCollectionStub()
                        .withParameterName('parameterToHave', false),
                    arguments: new FunctionCallArgumentCollectionStub()
                        .withArgument('parameterToHave', 'value-to-have')
                        .withArgument('parameterToIgnore', 'value-to-ignore'),
                    expectedArguments: [
                        new FunctionCallArgumentStub()
                            .withParameterName('parameterToHave').withArgumentValue('value-to-have'),
                    ],
                },
                {
                    name: 'without a provided argument',
                    expressionParameters: new FunctionParameterCollectionStub()
                        .withParameterName('parameterToHave', false)
                        .withParameterName('parameterToIgnore', true),
                    arguments: new FunctionCallArgumentCollectionStub()
                        .withArgument('parameterToHave', 'value-to-have'),
                    expectedArguments: [
                        new FunctionCallArgumentStub()
                            .withParameterName('parameterToHave').withArgumentValue('value-to-have'),
                    ],
                },
            ];
            for (const testCase of testCases) {
                it(testCase.name, () => {
                    let actual: IReadOnlyFunctionCallArgumentCollection;
                    const evaluatorMock: ExpressionEvaluator = (providedArgs) => {
                        actual = providedArgs;
                        return '';
                    };
                    const sut = new ExpressionBuilder()
                        .withEvaluator(evaluatorMock)
                        .withParameters(testCase.expressionParameters)
                        .build();
                    // act
                    sut.evaluate(testCase.arguments);
                    // assert
                    const actualArguments = actual.getAllParameterNames().map((name) => actual.getArgument(name));
                    expect(actualArguments).to.deep.equal(testCase.expectedArguments);
                });
            }
        });
    });
});

class ExpressionBuilder {
    private position: ExpressionPosition = new ExpressionPosition(0, 5);
    private parameters: IReadOnlyFunctionParameterCollection = new FunctionParameterCollectionStub();

    public withPosition(position: ExpressionPosition) {
        this.position = position;
        return this;
    }
    public withEvaluator(evaluator: ExpressionEvaluator) {
        this.evaluator = evaluator;
        return this;
    }
    public withParameters(parameters: IReadOnlyFunctionParameterCollection) {
        this.parameters = parameters;
        return this;
    }
    public withParameterName(parameterName: string, isOptional: boolean = true) {
        const collection = new FunctionParameterCollectionStub()
            .withParameterName(parameterName, isOptional);
        return this.withParameters(collection);
    }
    public withParameterNames(parameterNames: string[], isOptional: boolean = true) {
        const collection = new FunctionParameterCollectionStub()
            .withParameterNames(parameterNames, isOptional);
        return this.withParameters(collection);
    }
    public build() {
        return new Expression(this.position, this.evaluator, this.parameters);
    }

    private evaluator: ExpressionEvaluator = () => '';
}
