/**
 * Copyright (c) 2020-present, Goldman Sachs
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {
  type TestResult,
  type Service,
  TestPassed,
  TestFailed,
  ServiceTestSuite,
  TestData,
  ServiceTest,
  RunTestsTestableInput,
  AtomicTestId,
  DEFAULT_TEST_SUITE_PREFIX,
  DEFAULT_TEST_PREFIX,
  TestError,
  MultiExecutionServiceTestResult,
} from '@finos/legend-graph';
import {
  type GeneratorFn,
  addUniqueEntry,
  assertErrorThrown,
  filterByType,
  ActionState,
  deleteEntry,
  isNonNullable,
  generateEnumerableNameFromToken,
  getNullableFirstElement,
} from '@finos/legend-shared';
import { action, flow, makeObservable, observable } from 'mobx';
import type { EditorStore } from '../../../../EditorStore.js';
import {
  service_addTest,
  service_addTestSuite,
  service_deleteTestSuite,
} from '../../../../graphModifier/DSLService_GraphModifierHelper.js';
import { createEmptyEqualToJsonAssertion } from '../../../../shared/testable/TestableUtils.js';
import type { ServiceEditorState } from '../ServiceEditorState.js';
import { ServiceTestDataState } from './ServiceTestDataState.js';
import {
  SERIALIZATION_FORMAT,
  ServiceTestState,
} from './ServiceTestEditorState.js';

const createEmptyServiceTestSuite = (service: Service): ServiceTestSuite => {
  const suite = new ServiceTestSuite();
  suite.id = generateEnumerableNameFromToken(
    service.tests.map((s) => s.id),
    DEFAULT_TEST_SUITE_PREFIX,
  );
  suite.testData = new TestData();
  const test = new ServiceTest();
  test.serializationFormat = SERIALIZATION_FORMAT.PURE;
  test.id = generateEnumerableNameFromToken([], DEFAULT_TEST_PREFIX);
  test.__parent = suite;
  suite.tests = [test];
  const assertion = createEmptyEqualToJsonAssertion(test);
  test.assertions = [assertion];
  return suite;
};

export class ServiceTestSuiteState {
  readonly editorStore: EditorStore;
  readonly testableState: ServiceTestableState;
  suite: ServiceTestSuite;
  testDataState: ServiceTestDataState;
  selectedTestState: ServiceTestState | undefined;
  testStates: ServiceTestState[] = [];
  testToRename: ServiceTest | undefined;
  isRunningTest = ActionState.create();

  constructor(suite: ServiceTestSuite, testableState: ServiceTestableState) {
    makeObservable(this, {
      editorStore: false,
      testableState: false,
      testToRename: observable,
      selectedTestState: observable,
      setSelectedTestState: action,
      setTestToRename: action,
      addServiceTest: action,
      runSuite: flow,
      runFailingTests: flow,
    });

    this.editorStore = testableState.editorStore;
    this.testableState = testableState;
    this.suite = suite;
    this.testStates = this.suite.tests
      .filter(filterByType(ServiceTest))
      .map((test) => new ServiceTestState(this, test));
    this.selectedTestState = this.testStates[0];
    this.testDataState = new ServiceTestDataState(this.suite.testData, this);
  }

  setTestToRename(test: ServiceTest | undefined): void {
    this.testToRename = test;
  }

  setSelectedTestState(val: ServiceTestState | undefined): void {
    this.selectedTestState = val;
  }

  addServiceTest(): void {
    const test = new ServiceTest();
    test.serializationFormat = SERIALIZATION_FORMAT.PURE;
    test.id = generateEnumerableNameFromToken(
      this.suite.tests.map((t) => t.id),
      DEFAULT_TEST_PREFIX,
    );
    test.__parent = this.suite;
    const state = new ServiceTestState(this, test);
    state.addAssertion();
    this.selectedTestState = state;
    service_addTest(this.suite, test);
    addUniqueEntry(this.testStates, state);
  }

  deleteTest(testState: ServiceTestState): void {
    deleteEntry(this.suite.tests, testState.test);
    deleteEntry(this.testStates, testState);
    this.selectedTestState =
      this.selectedTestState === testState
        ? this.testStates[0]
        : this.selectedTestState;
  }

  *runSuite(): GeneratorFn<void> {
    try {
      this.isRunningTest.inProgress();
      this.testStates.forEach((t) => t.resetResult());
      this.testStates.forEach((t) => t.runningTestAction.inProgress());
      const service = this.testableState.serviceEditorState.service;
      const input = new RunTestsTestableInput(service);
      input.unitTestIds = this.suite.tests.map(
        (t) => new AtomicTestId(this.suite, t),
      );
      const testResults =
        (yield this.editorStore.graphManagerState.graphManager.runTests(
          [input],
          this.editorStore.graphManagerState.graph,
        )) as TestResult[];
      testResults.forEach((result) => {
        const state = this.testStates.find(
          (t) => t.test === result.atomicTestId.atomicTest,
        );
        state?.handleTestResult(result);
      });
      this.isRunningTest.complete();
    } catch (error) {
      assertErrorThrown(error);
      this.editorStore.applicationStore.notifyError(error);
      this.isRunningTest.fail();
    } finally {
      this.testStates.forEach((t) => t.runningTestAction.complete());
    }
  }

  *runFailingTests(): GeneratorFn<void> {
    try {
      this.isRunningTest.inProgress();
      const service = this.testableState.serviceEditorState.service;
      const input = new RunTestsTestableInput(service);
      input.unitTestIds = this.testStates
        .map((testState) => {
          const result = testState.testResultState.result;
          if (result instanceof TestFailed || result instanceof TestError) {
            testState.runningTestAction.inProgress();
            return new AtomicTestId(this.suite, testState.test);
          }
          return undefined;
        })
        .filter(isNonNullable);
      const testResults =
        (yield this.editorStore.graphManagerState.graphManager.runTests(
          [input],
          this.editorStore.graphManagerState.graph,
        )) as TestResult[];
      testResults.forEach((result) => {
        const state = this.testStates.find(
          (t) => t.test === result.atomicTestId.atomicTest,
        );
        state?.handleTestResult(result);
      });
      this.isRunningTest.complete();
    } catch (error) {
      assertErrorThrown(error);
      this.editorStore.applicationStore.notifyError(error);
      this.isRunningTest.fail();
    } finally {
      this.testStates.forEach((t) => t.runningTestAction.complete());
    }
  }

  get testCount(): number {
    return this.testStates.length;
  }

  get testPassed(): number {
    return this.testStates.filter(
      (e) =>
        e.testResultState.result instanceof TestPassed ||
        (e.testResultState.result instanceof MultiExecutionServiceTestResult &&
          Array.from(
            e.testResultState.result.keyIndexedTestResults.values(),
          ).every((kv) => kv instanceof TestPassed)),
    ).length;
  }

  get testFailed(): number {
    return this.testCount - this.testPassed;
  }
}

export class ServiceTestableState {
  readonly editorStore: EditorStore;
  readonly serviceEditorState: ServiceEditorState;
  selectedSuiteState: ServiceTestSuiteState | undefined;
  suiteToRename: ServiceTestSuite | undefined;

  constructor(
    editorStore: EditorStore,
    serviceEditorState: ServiceEditorState,
  ) {
    makeObservable(this, {
      editorStore: false,
      serviceEditorState: false,
      selectedSuiteState: observable,
      suiteToRename: observable,
      initSuites: action,
      addTestSuite: action,
      changeSuite: action,
      setSuiteToRename: action,
      deleteSuite: action,
    });
    this.editorStore = editorStore;
    this.serviceEditorState = serviceEditorState;
    this.initSuites();
  }

  setSuiteToRename(testSuite: ServiceTestSuite | undefined): void {
    this.suiteToRename = testSuite;
  }

  deleteSuite(testSuite: ServiceTestSuite): void {
    service_deleteTestSuite(this.serviceEditorState.service, testSuite);
    if (this.selectedSuiteState?.suite === testSuite) {
      this.selectedSuiteState = this.serviceEditorState.service.tests.length
        ? new ServiceTestSuiteState(
            this.serviceEditorState.service.tests[0] as ServiceTestSuite,
            this,
          )
        : undefined;
    }
  }

  changeSuite(suite: ServiceTestSuite): void {
    this.selectedSuiteState = new ServiceTestSuiteState(suite, this);
  }

  initSuites(): void {
    const serviceSuite = getNullableFirstElement(
      this.serviceEditorState.service.tests,
    );
    if (serviceSuite instanceof ServiceTestSuite) {
      this.selectedSuiteState = new ServiceTestSuiteState(serviceSuite, this);
    } else {
      this.selectedSuiteState = undefined;
    }
  }

  addTestSuite(): void {
    const suite = createEmptyServiceTestSuite(this.serviceEditorState.service);
    service_addTestSuite(
      this.serviceEditorState.service,
      suite,
      this.serviceEditorState.editorStore.changeDetectionState.observerContext,
    );
    this.selectedSuiteState = new ServiceTestSuiteState(suite, this);
  }
}
