/**
 * Copyright IBM Corp. 2024, 2025
 */

import { VCM } from '../../src/engine/variable-context-manager/context-manager.js';
import { ModelFactory } from '../../src/model-factories/model.factory.js';
import { Test } from '../../src/schemas/test.schema.js';
import {
  validAssertionData,
  validAssertionDataV2,
} from '../__mocks__/test-data/assertion.data.js';
import {
  validEnvironmentData,
  validEnvironmentDataV2,
} from '../__mocks__/test-data/environment.data.js';
import {
  gatewayEnv,
  multiGatewayAPIEnv,
  multiGatewayEnv,
} from '../__mocks__/test-data/gateway.data.js';
import {
  invalidTestSchema,
  testWithEmptyEnvironment,
  testWithEmptyAssertion,
  unsupportedKind,
  validTest,
  validTests,
  validTestWithEnvironmentsAndAssertions,
  validTestWithMultiAPIAndEnvironment,
} from '../__mocks__/test-data/test.data.js';

describe('ModelFactory', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('should validate session and undefined kind', () => {
    const env = {
      'PaymentAPI:1.0.1': ['https://localhost:3000/'],
    };
    const factory = new ModelFactory();
    factory.create([env]);
    expect(factory.getGateway()).toEqual(env);
    expect(factory.getAllTests()).toHaveLength(0);
    factory.destroy();
    expect(factory.getAllTests()).toHaveLength(0);
  });

  it('should store and return valid test', () => {
    const factory = new ModelFactory();
    factory.create([validTest]);

    const allTests = factory.getAllTests();
    expect(allTests).toHaveLength(1);
    expect(allTests[0].metadata!.name).toBe('TestPayments');
    expect(factory.getAllAssertions()).toHaveLength(0);
    expect(factory.getAllEnvironment()).toHaveLength(0);
    const {
      metadata: { namespace, name, version },
    } = validTest;
    const result = factory.getTest(namespace, name, version);
    expect(result).toBeDefined();
    expect(result?.metadata?.name).toBe(name);
  });

  it('should store and return valid assertions', () => {
    const factory = new ModelFactory();
    factory.create([validAssertionData]);

    const allTests = factory.getAllTests();
    expect(allTests).toHaveLength(0);
    expect(factory.getAllAssertions()).toHaveLength(1);
    expect(factory.getAllEnvironment()).toHaveLength(0);
    const {
      metadata: { namespace, name, version },
    } = validAssertionData;
    const result = factory.getAssertions(namespace, name, version);
    expect(result).toBeDefined();
    expect(result?.metadata?.name).toBe(name);
  });

  it('should store and return valid environment', () => {
    const factory = new ModelFactory();
    factory.create([validEnvironmentData]);
    const allTests = factory.getAllTests();
    expect(allTests).toHaveLength(0);
    expect(factory.getAllAssertions()).toHaveLength(0);
    expect(factory.getAllEnvironment()).toHaveLength(1);
    const {
      metadata: { namespace, name, version },
    } = validEnvironmentData;
    const result = factory.getEnvironment(namespace, name, version);
    expect(result).toBeDefined();
    expect(result?.metadata?.name).toBe(name);
  });

  it('should throw for invalid schema', () => {
    const factory = new ModelFactory();
    expect(() => factory.create([invalidTestSchema])).toThrow();
  });

  it('should ignore or throw error for unsupported kind', () => {
    const factory = new ModelFactory();

    expect(() => factory.create([unsupportedKind])).toThrow(); // Assuming you silently ignore
    expect(factory.getAllTests()).toHaveLength(0);
    expect(factory.getAllAssertions()).toHaveLength(0);
    expect(factory.getAllEnvironment()).toHaveLength(0);
  });
});

describe('ModelFactory.resolveRefs', () => {
  let factory: ModelFactory;

  beforeEach(() => {
    factory = new ModelFactory();
  });

  it('should resolve API, environment, and assertions for each test ', () => {
    factory.create(gatewayEnv);
    factory.create([validAssertionData]);
    factory.create([validEnvironmentData]);
    factory.create([validTest]);
    factory.resolveRefs();
    const getTest = factory.getAllTests();
    expect(getTest).toHaveLength(1);
    // Get the environment data to check whether same data is modelled.
    const env = factory.getEnvironment(
      'default',
      'TestPaymentsEnvironment',
      '1.0.0',
    );
    env?.spec?.variables!.forEach((variable) => {
      const resolvedEnvironment = getTest[0].spec.environment as {
        variables: Array<{
          spec: { variables: any[] };
        }>;
      };
      expect(resolvedEnvironment.variables[0].spec.variables).toContainEqual(
        variable,
      );
      expect(
        VCM.createContext(getTest[0].vcmId!).get(variable.key!),
      ).toMatchObject({
        value: variable.value,
      });
    });

    // Get the assertion data to check whether same data is modelled.
    const assertions = factory.getAssertions(
      'default',
      'TestPaymentAssertion',
      '1.0.0',
    );
    assertions?.spec!.forEach((assert) => {
      const requestAssertions = getTest[0].spec.request[0].assertions;
      const resolvedAssertions =
        requestAssertions && 'assertions' in requestAssertions
          ? requestAssertions.assertions
          : [];
      expect(resolvedAssertions).toBeDefined();
      if (resolvedAssertions) {
        const allAssertionSpecs = resolvedAssertions.flatMap((a) => a.spec);
        expect(allAssertionSpecs).toContainEqual(assert);
      }
    });
  });

  it('should resolve API, environment, and assertions for each multi gateway and multiple environment test ', () => {
    factory.create(gatewayEnv);
    factory.create([validAssertionData, validAssertionDataV2]);
    factory.create([validEnvironmentData, validEnvironmentDataV2]);
    factory.create([validTestWithEnvironmentsAndAssertions]);
    factory.resolveRefs();
    const getTests = factory.getAllTests();
    expect(getTests).toHaveLength(2);
    expect(getTests[0].vcmId).not.toEqual(getTests[1].vcmId);
  });

  it('should resolve 2 endpoints with 1 environment ', () => {
    factory.create(multiGatewayEnv);
    factory.create([validAssertionData]);
    factory.create([validEnvironmentData]);
    factory.create([validTest]);
    factory.create([validEnvironmentDataV2]);
    factory.resolveRefs();
    const getTests = factory.getAllTests();
    expect(getTests).toHaveLength(2);
  });

  it('should resolve 2 gateway endpoints with 2 test suite', () => {
    factory.create(multiGatewayEnv);
    factory.create([validAssertionData]);
    factory.create([validEnvironmentData]);
    const patchedValidTests = validTests.map((t) => ({
      ...t,
      spec: {
        ...t.spec,
        environment: { $ref: 'default:TestPaymentsEnvironment:1.0.0' },
      },
    }));
    factory.create(patchedValidTests);
    factory.create([validEnvironmentDataV2]);
    factory.resolveRefs();
    const getTests = factory.getAllTests();
    expect(getTests).toHaveLength(4);
  });

  it('should resolve 1 endpoint and 2 environments ', () => {
    factory.create([validAssertionData, validAssertionDataV2]);
    factory.create([validEnvironmentData, validEnvironmentDataV2]);
    factory.create([validTestWithEnvironmentsAndAssertions]);
    factory.create([validEnvironmentDataV2]);
    factory.resolveRefs();
    const getTests = factory.getAllTests();
    expect(getTests).toHaveLength(2);
  });

  it('should resolve 2x2 api gateway, 2 environments ', () => {
    factory.create(multiGatewayAPIEnv);
    factory.create([validAssertionData]);
    factory.create([validEnvironmentData]);
    factory.create([validTestWithMultiAPIAndEnvironment]);
    factory.create([validEnvironmentDataV2]);
    factory.resolveRefs();
    const getTests = factory.getAllTests();
    expect(getTests).toHaveLength(8);
    // All test should have unique VCM to store test results
    const vcmIds = getTests.map((test) => test.vcmId);
    const uniqueIds = new Set(vcmIds);
    expect(uniqueIds.size).toBe(vcmIds.length);
  });

  it('should correctly resolve array of $ref objects in assertions', () => {
    factory.create([validAssertionData, validAssertionDataV2]);
    factory.create([validEnvironmentData]);

    // Create a test with array of assertion ref objects
    const testWithArrayOfRefObjects = {
      kind: 'test',
      metadata: {
        name: 'TestWithArrayOfRefObjects',
        version: '1.0.0',
        namespace: 'default',
      },
      spec: {
        api: {
          $endpoint: 'www.test.com',
        },
        environment: {
          $ref: 'default:TestPaymentsEnvironment:1.0.0',
        },
        request: [
          {
            method: 'POST',
            resource: '/api/resource',
            assertions: [
              { $ref: 'default:TestPaymentAssertion:1.0.0' },
              { $ref: 'default:TestPaymentAssertion:2.0.0' },
            ],
          },
        ],
      },
    };

    factory.create([testWithArrayOfRefObjects]);
    factory.resolveRefs();

    const tests = factory.getAllTests();
    expect(tests).toHaveLength(1);

    // Verify that both assertion references were resolved correctly
    const assertions1 = factory.getAssertions(
      'default',
      'TestPaymentAssertion',
      '1.0.0',
    )?.spec;
    const assertions2 = factory.getAssertions(
      'default',
      'TestPaymentAssertion',
      '2.0.0',
    )?.spec;

    // Check that all assertions from both references are included in the expressions
    const assertions = tests[0].spec.request[0].assertions;
    const expressions =
      assertions && 'assertions' in assertions ? assertions.assertions : [];

    assertions1?.forEach((assertion) => {
      expect(expressions?.[0].spec).toContainEqual(
        expect.objectContaining({
          name: assertion.name,
          key: assertion.key,
          value: assertion.value,
          action: assertion.action,
        }),
      );
    });

    assertions2?.forEach((assertion) => {
      expect(expressions?.[0].spec).toContainEqual(
        expect.objectContaining({
          name: assertion.name,
          key: assertion.key,
          value: assertion.value,
          action: assertion.action,
        }),
      );
    });
  });

  it('should resolve API, environment, and assertions for test collection ', () => {
    factory.create(gatewayEnv);
    factory.create([validAssertionData]);
    factory.create([validEnvironmentData]);
    // ensure environment is a valid $ref
    const patchedValidTests = validTests.map((t) => ({
      ...t,
      spec: {
        ...t.spec,
        environment: { $ref: 'default:TestPaymentsEnvironment:1.0.0' },
      },
    }));
    factory.create(patchedValidTests);
    factory.resolveRefs();
    const getTests = factory.getAllTests();
    expect(getTests).toHaveLength(2);
    if ('variables' in patchedValidTests[1].spec.environment) {
      expect(
        (getTests[0].spec.environment as { variables: any[] }).variables,
      ).toContainEqual(
        (patchedValidTests[1].spec.environment as { variables: any[] })
          .variables[0],
      );
    }

    const assertions = factory.getAssertions(
      'default',
      'TestPaymentAssertion',
      '1.0.0',
    );
    assertions?.spec!.forEach((assert) => {
      const requestAssertions0 = getTests[0].spec.request[0].assertions;
      const resolvedAssertions1 =
        requestAssertions0 && 'assertions' in requestAssertions0
          ? requestAssertions0.assertions
          : [];
      expect(resolvedAssertions1).toBeDefined();
      if (resolvedAssertions1) {
        const allAssertionSpecs1 = resolvedAssertions1.flatMap((a) => a.spec);
        expect(allAssertionSpecs1).toContainEqual(assert);
      }
      const requestAssertions2 = getTests[1].spec.request[0].assertions;
      const resolvedAssertions2 =
        requestAssertions2 && 'assertions' in requestAssertions2
          ? requestAssertions2.assertions
          : [];
      expect(resolvedAssertions2).toBeDefined();
      if (resolvedAssertions2) {
        const allAssertionSpecs2 = resolvedAssertions2.flatMap((a) => a.spec);
        expect(allAssertionSpecs2).toContainEqual(assert);
      }
    });
  });

  it('should handle missing environment', () => {
    expect(() => factory.create([testWithEmptyEnvironment])).toThrow(
      `Validation error at spec.environment.$ref: $ref cannot be an empty string`,
    );
  });

  it('should handle missing assertion', () => {
    expect(() => factory.create([testWithEmptyAssertion])).toThrow(
      `Validation error at spec.request.[0].assertions.$ref: $ref cannot be an empty string`,
    );
  });

  it('should handle error in resolving values', () => {
    factory.create([
      {
        ...validTest,
        spec: {
          ...validTest.spec,
          api: {
            $ref: 'PaymentAPI:3.0.1',
          },
        },
      },
    ]);
    expect(() => factory.resolveRefs()).toThrow(
      'default:TestPaymentAssertion:1.0.0 is not available in assertions',
    );

    factory.create([validAssertionData]);
    expect(() => factory.resolveRefs()).toThrow(
      `Reference variable 'PaymentAPI:3.0.1' not defined`,
    );

    factory.create([
      {
        'PaymentAPI:3.0.1': [],
      },
    ]);

    expect(() => factory.resolveRefs()).not.toThrow();
  });

  it('should handle invalid environment resolution', () => {
    factory.create([
      {
        ...validTest,
        spec: {
          ...validTest.spec,
          environment: {
            $ref: 'default:TestPaymentsEnvironment:4.0.0',
          },
        },
      },
    ]);
    factory.create(gatewayEnv);
    factory.create([validAssertionData]);

    expect(() => factory.resolveRefs()).toThrow(
      'default:TestPaymentsEnvironment:4.0.0 is not available in environment',
    );
  });

  it('should do nothing if no test s are present', () => {
    jest.spyOn(factory, 'getAllTests').mockReturnValue([]);
    jest.spyOn(factory as any, 'resolveAPI').mockImplementation(jest.fn());
    jest
      .spyOn(factory as any, 'resolveEnvironment')
      .mockImplementation(jest.fn());
    jest
      .spyOn(factory as any, 'resolveAssertions')
      .mockImplementation(jest.fn());

    factory.resolveRefs();

    expect((factory as any).resolveAPI).not.toHaveBeenCalled();
    expect((factory as any).resolveEnvironment).not.toHaveBeenCalled();
    expect((factory as any).resolveAssertions).not.toHaveBeenCalled();
  });

  it('should handle a single test  gracefully', () => {
    jest
      .spyOn(factory, 'getAllTestsWithKey')
      .mockReturnValue([{ key: 'key1', test: validTests[0] as Test }]);
    jest.spyOn(factory as any, 'resolveAPI').mockImplementation(jest.fn());
    jest
      .spyOn(factory as any, 'resolveEnvironment')
      .mockImplementation(jest.fn());
    jest
      .spyOn(factory as any, 'resolveAssertions')
      .mockImplementation(jest.fn());
    factory.resolveRefs();

    expect((factory as any).resolveAPI).toHaveBeenCalledTimes(1);
    expect((factory as any).resolveEnvironment).toHaveBeenCalledTimes(1);
    expect((factory as any).resolveAssertions).toHaveBeenCalledTimes(1);
  });

  it('should handle clearing the factory, when destroy is called', () => {
    factory.create([validTest]);
    factory.destroy();

    expect(factory.getAllTests()).toHaveLength(0);
  });
});
