import { describe, it, expect } from 'vitest';
import type { FunctionData } from '@/application/collections/';
import { createScriptCompiler } from '@/application/Application/Loader/Collections/Compiler/Executable/Script/Compiler/ScriptCompilerFactory';
import type { FunctionCallCompiler } from '@/application/Application/Loader/Collections/Compiler/Executable/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler';
import { createFunctionDataWithCode } from '@tests/unit/shared/Stubs/FunctionDataStub';
import { FunctionCallCompilerStub } from '@tests/unit/shared/Stubs/FunctionCallCompilerStub';
import { createSharedFunctionsParserStub } from '@tests/unit/shared/Stubs/SharedFunctionsParserStub';
import { SharedFunctionCollectionStub } from '@tests/unit/shared/Stubs/SharedFunctionCollectionStub';
import { parseFunctionCalls } from '@/application/Application/Loader/Collections/Compiler/Executable/Script/Compiler/Function/Call/FunctionCallsParser';
import { FunctionCallDataStub } from '@tests/unit/shared/Stubs/FunctionCallDataStub';
import type { CodeValidator } from '@/application/Application/Loader/Collections/Compiler/Executable/Script/Validation/CodeValidator';
import { CodeValidatorStub } from '@tests/unit/shared/Stubs/CodeValidatorStub';
import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub';
import { createScriptDataWithCall, createScriptDataWithCode } from '@tests/unit/shared/Stubs/ScriptDataStub';
import type { ErrorWithContextWrapper } from '@/application/Application/Loader/Collections/Compiler/Common/ContextualError';
import { errorWithContextWrapperStub } from '@tests/unit/shared/Stubs/ErrorWithContextWrapperStub';
import { ScriptCodeStub } from '@tests/unit/shared/Stubs/ScriptCodeStub';
import type { ScriptCodeFactory } from '@/domain/Executables/Script/Code/ScriptCodeFactory';
import { createScriptCodeFactoryStub } from '@tests/unit/shared/Stubs/ScriptCodeFactoryStub';
import { itThrowsContextualError } from '@tests/unit/application/Application/Loader/Collections/Compiler/Common/ContextualErrorTester';
import type { SharedFunctionsParser } from '@/application/Application/Loader/Collections/Compiler/Executable/Script/Compiler/Function/SharedFunctionsParser';
import type { ScriptCompiler } from '@/application/Application/Loader/Collections/Compiler/Executable/Script/Compiler/ScriptCompiler';
import { ScriptLanguage } from '@/domain/ScriptMetadata/ScriptLanguage';
import { CodeValidationRule } from '@/application/Application/Loader/Collections/Compiler/Executable/Script/Validation/CodeValidationRule';

describe('ScriptCompilerFactory', () => {
  describe('createScriptCompiler', () => {
    describe('canCompile', () => {
      it('returns true if "call" is defined', () => {
        // arrange
        const sut = new TestContext()
          .withEmptyFunctions()
          .create();
        const script = createScriptDataWithCall();
        // act
        const actual = sut.canCompile(script);
        // assert
        expect(actual).to.equal(true);
      });
      it('returns false if "call" is undefined', () => {
        // arrange
        const sut = new TestContext()
          .withEmptyFunctions()
          .create();
        const script = createScriptDataWithCode();
        // act
        const actual = sut.canCompile(script);
        // assert
        expect(actual).to.equal(false);
      });
    });
    describe('compile', () => {
      it('throws if script does not have body', () => {
        // arrange
        const expectedError = 'Script does include any calls.';
        const scriptData = createScriptDataWithCode();
        const sut = new TestContext()
          .withSomeFunctions()
          .create();
        // act
        const act = () => sut.compile(scriptData);
        // assert
        expect(act).to.throw(expectedError);
      });
      describe('code construction', () => {
        it('returns code from the factory', () => {
          // arrange
          const expectedCode = new ScriptCodeStub();
          const scriptCodeFactory = () => expectedCode;
          const sut = new TestContext()
            .withSomeFunctions()
            .withScriptCodeFactory(scriptCodeFactory)
            .create();
          // act
          const actualCode = sut.compile(createScriptDataWithCall());
          // assert
          expect(actualCode).to.equal(expectedCode);
        });
        it('creates code correctly', () => {
          // arrange
          const expectedCode = 'expected-code';
          const expectedRevertCode = 'expected-revert-code';
          let actualCode: string | undefined;
          let actualRevertCode: string | undefined;
          const scriptCodeFactory = (code: string, revertCode: string) => {
            actualCode = code;
            actualRevertCode = revertCode;
            return new ScriptCodeStub();
          };
          const call = new FunctionCallDataStub();
          const script = createScriptDataWithCall(call);
          const functions = [createFunctionDataWithCode().withName('existing-func')];
          const compiledFunctions = new SharedFunctionCollectionStub();
          const functionParserMock = createSharedFunctionsParserStub();
          functionParserMock.setup(functions, compiledFunctions);
          const callCompilerMock = new FunctionCallCompilerStub();
          callCompilerMock.setup(
            parseFunctionCalls(call),
            compiledFunctions,
            new CompiledCodeStub()
              .withCode(expectedCode)
              .withRevertCode(expectedRevertCode),
          );
          const sut = new TestContext()
            .withFunctions(...functions)
            .withSharedFunctionsParser(functionParserMock.parser)
            .withFunctionCallCompiler(callCompilerMock)
            .withScriptCodeFactory(scriptCodeFactory)
            .create();
          // act
          sut.compile(script);
          // assert
          expect(actualCode).to.equal(expectedCode);
          expect(actualRevertCode).to.equal(expectedRevertCode);
        });
      });
      describe('parses functions as expected', () => {
        it('parses functions with expected language', () => {
          // arrange
          const expectedLanguage = ScriptLanguage.batchfile;
          const functionParserMock = createSharedFunctionsParserStub();
          const sut = new TestContext()
            .withSomeFunctions()
            .withLanguage(expectedLanguage)
            .withSharedFunctionsParser(functionParserMock.parser)
            .create();
          const scriptData = createScriptDataWithCall();
          // act
          sut.compile(scriptData);
          // assert
          const parserCalls = functionParserMock.callHistory;
          expect(parserCalls.length).to.equal(1);
          const actualLanguage = parserCalls[0].language;
          expect(actualLanguage).to.equal(expectedLanguage);
        });
        it('parses given functions', () => {
          // arrange
          const expectedFunctions = [createFunctionDataWithCode().withName('existing-func')];
          const functionParserMock = createSharedFunctionsParserStub();
          const sut = new TestContext()
            .withFunctions(...expectedFunctions)
            .withSharedFunctionsParser(functionParserMock.parser)
            .create();
          const scriptData = createScriptDataWithCall();
          // act
          sut.compile(scriptData);
          // assert
          const parserCalls = functionParserMock.callHistory;
          expect(parserCalls.length).to.equal(1);
          expect(parserCalls[0].functions).to.deep.equal(expectedFunctions);
        });
      });
      describe('rethrows error with script name', () => {
        // arrange
        const scriptName = 'scriptName';
        const expectedErrorMessage = `Failed to compile script: ${scriptName}`;
        const expectedInnerError = new Error();
        const callCompiler: FunctionCallCompiler = {
          compileFunctionCalls: () => { throw expectedInnerError; },
        };
        const scriptData = createScriptDataWithCall()
          .withName(scriptName);
        const builder = new TestContext()
          .withSomeFunctions()
          .withFunctionCallCompiler(callCompiler);
        itThrowsContextualError({
          // act
          throwingAction: (wrapError) => {
            builder
              .withErrorWrapper(wrapError)
              .create()
              .compile(scriptData);
          },
          // assert
          expectedWrappedError: expectedInnerError,
          expectedContextMessage: expectedErrorMessage,
        });
      });
      describe('rethrows error from script code factory with script name', () => {
        // arrange
        const scriptName = 'scriptName';
        const expectedErrorMessage = `Failed to compile script: ${scriptName}`;
        const expectedInnerError = new Error();
        const scriptCodeFactory: ScriptCodeFactory = () => {
          throw expectedInnerError;
        };
        const scriptData = createScriptDataWithCall()
          .withName(scriptName);
        const builder = new TestContext()
          .withSomeFunctions()
          .withScriptCodeFactory(scriptCodeFactory);
        itThrowsContextualError({
          // act
          throwingAction: (wrapError) => {
            builder
              .withErrorWrapper(wrapError)
              .create()
              .compile(scriptData);
          },
          // assert
          expectedWrappedError: expectedInnerError,
          expectedContextMessage: expectedErrorMessage,
        });
      });
      describe('compiled code validation', () => {
        it('validates compiled code', () => {
          // arrange
          const expectedExecuteCode = 'execute code to be validated';
          const expectedRevertCode = 'revert code to be validated';
          const scriptData = createScriptDataWithCall();
          const validator = new CodeValidatorStub();
          const sut = new TestContext()
            .withSomeFunctions()
            .withCodeValidator(validator.get())
            .withFunctionCallCompiler(
              new FunctionCallCompilerStub()
                .withDefaultCompiledCode(
                  new CompiledCodeStub()
                    .withCode(expectedExecuteCode)
                    .withRevertCode(expectedRevertCode),
                ),
            )
            .create();
          // act
          sut.compile(scriptData);
          // assert
          validator.assertValidatedCodes([
            expectedExecuteCode, expectedRevertCode,
          ]);
        });
        it('applies correct validation rules', () => {
          // arrange
          const expectedRules: readonly CodeValidationRule[] = [
            CodeValidationRule.NoEmptyLines,
            CodeValidationRule.NoTooLongLines,
            // Allow duplicated lines to enable calling same function multiple times
            CodeValidationRule.NoCommentOnlyLines,
          ];
          const scriptData = createScriptDataWithCall();
          const validator = new CodeValidatorStub();
          const sut = new TestContext()
            .withSomeFunctions()
            .withCodeValidator(validator.get())
            .create();
          // act
          sut.compile(scriptData);
          // assert
          validator.assertValidatedRules(expectedRules);
        });
        it('validates for correct scripting language', () => {
          // arrange
          const expectedLanguage: ScriptLanguage = ScriptLanguage.shellscript;
          const scriptData = createScriptDataWithCall();
          const validator = new CodeValidatorStub();
          const sut = new TestContext()
            .withSomeFunctions()
            .withLanguage(expectedLanguage)
            .withCodeValidator(validator.get())
            .create();
          // act
          sut.compile(scriptData);
          // assert
          validator.assertValidatedLanguage(expectedLanguage);
        });
      });
    });
  });
});

class TestContext {
  private static createFunctions(...names: string[]): FunctionData[] {
    return names.map((functionName) => {
      return createFunctionDataWithCode().withName(functionName);
    });
  }

  private functions: FunctionData[] | undefined;

  private language: ScriptLanguage = ScriptLanguage.batchfile;

  private sharedFunctionsParser: SharedFunctionsParser = createSharedFunctionsParserStub().parser;

  private callCompiler: FunctionCallCompiler = new FunctionCallCompilerStub();

  private codeValidator: CodeValidator = new CodeValidatorStub()
    .get();

  private wrapError: ErrorWithContextWrapper = errorWithContextWrapperStub;

  private scriptCodeFactory: ScriptCodeFactory = createScriptCodeFactoryStub({
    defaultCodePrefix: TestContext.name,
  });

  public withFunctions(...functions: FunctionData[]): this {
    this.functions = functions;
    return this;
  }

  public withSomeFunctions(): this {
    this.functions = TestContext.createFunctions('test-function');
    return this;
  }

  public withFunctionNames(...functionNames: string[]): this {
    this.functions = TestContext.createFunctions(...functionNames);
    return this;
  }

  public withEmptyFunctions(): this {
    this.functions = [];
    return this;
  }

  public withLanguage(language: ScriptLanguage): this {
    this.language = language;
    return this;
  }

  public withSharedFunctionsParser(
    sharedFunctionsParser: SharedFunctionsParser,
  ): this {
    this.sharedFunctionsParser = sharedFunctionsParser;
    return this;
  }

  public withCodeValidator(
    codeValidator: CodeValidator,
  ): this {
    this.codeValidator = codeValidator;
    return this;
  }

  public withFunctionCallCompiler(callCompiler: FunctionCallCompiler): this {
    this.callCompiler = callCompiler;
    return this;
  }

  public withErrorWrapper(wrapError: ErrorWithContextWrapper): this {
    this.wrapError = wrapError;
    return this;
  }

  public withScriptCodeFactory(scriptCodeFactory: ScriptCodeFactory): this {
    this.scriptCodeFactory = scriptCodeFactory;
    return this;
  }

  public create(): ScriptCompiler {
    if (!this.functions) {
      throw new Error('Function behavior not defined');
    }
    return createScriptCompiler({
      categoryContext: {
        functions: this.functions,
        language: this.language,
      },
      utilities: {
        sharedFunctionsParser: this.sharedFunctionsParser,
        callCompiler: this.callCompiler,
        codeValidator: this.codeValidator,
        wrapError: this.wrapError,
        scriptCodeFactory: this.scriptCodeFactory,
      },
    });
  }
}
