/**
 * Copyright IBM Corp. 2024, 2025
 */
import {
  ContextManager,
  VCM,
} from '../../../src/engine/variable-context-manager/context-manager.js';

describe('ContextManager with multiple contexts', () => {
  let manager: ContextManager;

  beforeAll(() => {
    manager = new ContextManager();
  });

  afterEach(() => {
    manager.clearAll();
  });

  it('should isolate data between contexts', () => {
    const contextA = manager.createContext('contextA');
    const contextB = manager.createContext('contextB');
    manager.getGlobalContext().setVariable('env', 'prod');

    contextA.setVariable('user', 'Alice');
    contextB.setVariable('user', 'Bob');

    expect(manager.resolve('contextA', 'User: ${user}')).toBe('User: Alice');
    expect(manager.resolve('contextB', 'User: ${user}')).toBe('User: Bob');
    expect(manager.resolve('contextA', 'Env: ${env}')).toBe('Env: prod');
    expect(manager.resolve('contextA', 'Env: ${env}')).toBe('Env: prod');
    expect(() => manager.resolve('contextC', 'Env: ${env}')).toThrow();
    const contexts = manager.listContexts();
    expect(contexts.sort()).toEqual(['contextA', 'contextB']);
  });

  it('should support array index path resolution for given expression', () => {
    const input = [{ name: 'Alice' }, { name: 'Bob' }];
    const context = manager.createContext('s-array');
    context.setVariable('users', input);

    const resolved = manager.resolve(
      's-array',
      'First: ${users.0.name}, Second: ${users.1.name}',
    );
    expect(() => manager.resolveExpression('s-array', null)).toThrow();
    expect(manager.resolveExpression('s-array', 'users')).toBe(input);
    expect(manager.resolveExpression('s-array', 'users.0.name')).toBe('Alice');
    expect(manager.resolve('s-array', '${users.0.name}')).toBe('Alice');
    expect(resolved).toBe('First: Alice, Second: Bob');
    expect(manager.resolve('s-array', '${users.0.name}A')).toBe('AliceA');
  });

  it('should resolve given string', () => {
    const input = 'Alice';
    const context = manager.createContext('s-array');
    context.setVariable('users', input);
    expect(manager.resolve('s-array', '${users}A')).toBe('AliceA');
  });

  it('check input as undefined', () => {
    manager.createContext('s-array');
    expect(manager.resolve('s-array', undefined)).toBe(undefined);
  });
});

describe('ContextManager - Negative & Error Scenarios', () => {
  let manager: ContextManager;

  beforeAll(() => {
    manager = new ContextManager();
  });
  afterEach(() => {
    manager.clearAll();
  });

  it('should throw error for no context', () => {
    expect(() => {
      manager.resolve('test', 'Hello ${missing}');
    }).toThrow("Context 'test' not found.");
    expect(() => {
      manager.resolveExpression('test', 'Hello ${missing}');
    }).toThrow("Context 'test' not found.");
  });

  it('should throw context exists', () => {
    expect(() => {
      manager.createContext('test');
      manager.createContext('test');
    }).toBeDefined();
  });

  it('should not throw error for undefined variable', () => {
    expect(() => {
      manager.getContext('test');
      manager.resolve('test', 'Hello ${missing}');
    }).not.toThrow();
  });

  it('should not throw error for empty variable', () => {
    manager.createContext('test');
    expect(() => {
      manager.resolve('test', '');
    }).not.toThrow();
    expect(manager.resolve('test', '')).toEqual('');

    expect(() => manager.resolve('test', '${}')).not.toThrow();
    expect(manager.resolve('test', '${}')).toEqual('');

    expect(() => manager.resolve('test', '${""}')).not.toThrow();
    expect(manager.resolve('test', '${""}')).toBeUndefined();
  });

  it('should not throw error for empty variable in expression', () => {
    manager.createContext('test');
    expect(() => {
      manager.resolveExpression('test', '');
    }).not.toThrow();
    expect(manager.resolveExpression('test', '')).toEqual('');

    expect(() => manager.resolveExpression('test', '${}')).not.toThrow();
    expect(manager.resolveExpression('test', '${}')).toBeUndefined();

    expect(() => manager.resolveExpression('test', '${""}')).not.toThrow();
    expect(manager.resolveExpression('test', '${""}')).toBeUndefined();
  });

  it('should handle invalid path on object gracefully', () => {
    const context = manager.createContext('test');
    context.setVariable('user', { name: 'Alice' });

    const result = manager.resolve('test', 'Hello ${user.age.year}');
    expect(result).toBe('Hello ');
  });

  it('should handle invalid array index gracefully', () => {
    const context = manager.createContext('array');
    context.setVariable('items', ['apple', 'banana']);

    const result = manager.resolve('array', 'Item: ${items.5}');
    expect(result).toBe('Item: ');
  });
});

describe('VCM - Validate all scenarios using global store', () => {
  beforeAll(() => {
    VCM.createContext('contextA').setVariable('user', 'Alice');
    VCM.createContext('contextB').setVariable('user', 'Bob');
    VCM.createContext('contextA')?.setVariable('userJson', {
      name: 'Alice',
      address: { city: 'London', country: 'UK' },
    });
    VCM.createContext('contextA')?.setVariable('userLocJson', {
      cityKey: 'city',
    });
    VCM.createContext('contextA')?.setVariable('fruits', [
      'apple',
      'banana',
      'cherry',
    ]);
    VCM.createContext('contextA')?.setVariable('userFav', {
      favorites: { index: 1 },
    });
    VCM.createContext('contextA')?.setVariable('scores', [10, false, 30]);
  });

  it('should isolate data between contexts and fetch using VCM', () => {
    expect(VCM.resolve('contextA', 'User: ${user}')).toBe('User: Alice');
    expect(VCM.resolve('contextB', 'User: ${user}')).toBe('User: Bob');
  });

  it('should resolve JSON using VCM', () => {
    const input = {
      greeting: 'Hello ${userJson.name}',
      details: {
        city: '${userJson.address.city}',
      },
    };
    const output = {
      greeting: 'Hello Alice',
      details: {
        city: 'London',
      },
    };
    expect(VCM.resolve('contextA', 'User: ${user}')).toBe('User: Alice');
    expect(VCM.resolve('contextB', 'User: ${user}')).toBe('User: Bob');
    expect(VCM.resolve('contextA', input)).toEqual(output);
  });

  it('should resolve JSON with nested using VCM', () => {
    const input = {
      greeting: 'Hello ${userJson.name}',
      details: {
        city: '${userJson.address.${userLocJson.cityKey}}',
      },
    };
    const output = {
      greeting: 'Hello Alice',
      details: {
        city: 'London',
      },
    };
    expect(VCM.resolve('contextA', 'User: ${user}')).toBe('User: Alice');
    expect(VCM.resolve('contextB', 'User: ${user}')).toBe('User: Bob');
    expect(VCM.resolve('contextA', input)).toEqual(output);
  });

  it('should resolve Array using VCM with text and variable replacement', () => {
    const input = ['first ${fruits.1}'];

    const output = ['first banana'];
    expect(VCM.resolve('contextA', input)).toEqual(output);
  });

  it('should resolve Array using VCM with full multiple variable replacement', () => {
    const input = ['${fruits.0} ${fruits.1}'];

    const output = ['apple banana'];
    expect(VCM.resolve('contextA', input)).toEqual(output);
  });

  it('should resolve Array using VCM with nested variable replacement', () => {
    const input = ['${fruits.${userFav.favorites.index}}'];

    const output = ['banana'];
    expect(VCM.resolve('contextA', input)).toEqual(output);
  });

  it('should resolve Array using VCM with text and nest variable replacement', () => {
    const input = ['Second: ${fruits.${userFav.favorites.index}}'];

    const output = ['Second: banana'];
    expect(VCM.resolve('contextA', input)).toEqual(output);
  });

  it('should resolve arrays inside an object structure', () => {
    const input = {
      best: '${scores.2}',
      worst: '${scores.0}',
      bool: '${scores.1}',
    };
    const expected = {
      best: 30,
      worst: 10,
      bool: false,
    };
    expect(VCM.resolve('contextA', input)).toEqual(expected);
  });

  it('should handle out-of-bound index gracefully', () => {
    expect(VCM.resolve('contextA', 'Invalid: num ${nums.5}')).toBe(
      'Invalid: num ',
    );
  });

  it('Clean all and check no state is available', () => {
    VCM.clearAll();
    expect(() => {
      VCM.resolve('test', 'Hello ${missing}');
    }).toThrow("Context 'test' not found.");
  });

  it('should throw error for no context', () => {
    expect(() => {
      VCM.resolve('test', 'Hello ${missing}');
    }).toThrow("Context 'test' not found.");
  });

  it('should handle undefined variable gracefully', () => {
    VCM.createContext('test');
    expect(VCM.resolve('test', 'Hello ${missing}')).toBe('Hello ');
  });
});

describe('VCM.loadEnv', () => {
  beforeAll(() => {
    VCM.createContext('testContext');
  });

  it('should load environment variables into testContext', () => {
    const envVars = [
      {
        key: 'content-type',
        value: 'application/json',
        isSecret: true,
      },
      {
        key: 'user',
        value: 'Alice',
        isSecret: false,
      },
    ];

    VCM.loadEnv('testContext', envVars);

    const context = VCM.createContext('testContext');
    expect(context).toBeDefined();
    expect(context?.get('user')?.value).toBe('Alice');
    expect(context?.getValue('content-type')).toBe('application/json');
    expect(context?.get('content-type')).toEqual({
      value: 'application/json',
      isSecret: true,
    });
  });

  it('should load variables into testContext', () => {
    const envVars = [
      {
        key: 'content-type',
        value: 'application/json',
        isSecret: true,
      },
      {
        key: 'user',
        value: 'Alice',
        isSecret: false,
      },
      {
        key: 'method',
        value: 'GET',
      },
    ];

    VCM.loadEnv('testContext', envVars);

    const context = VCM.createContext('testContext');
    expect(context).toBeDefined();
    expect(context?.get('user')?.value).toBe('Alice');
    expect(context?.getValue('content-type')).toBe('application/json');
    expect(context?.get('content-type')).toEqual({
      value: 'application/json',
      isSecret: true,
    });
  });

  it('should resolve "TEST" request model', () => {
    const input = {
      request: [
        {
          method: 'POST',
          resource: 'v2/pet',
          headers: [
            {
              key: 'Content-Type',
              value: '${content-type}',
            },
          ],
          payload: {
            raw: {
              json: '{\n  "name":"Jose"\n}\n',
            },
          },
        },
        {
          method: '${method}',
          headers: [
            {
              key: 'Content-Type',
              value: '${content-type}',
            },
          ],
          payload: {
            raw: {
              json: { name: 'Adam' },
            },
          },
        },
      ],
    };

    const output = {
      request: [
        {
          method: 'POST',
          resource: 'v2/pet',
          headers: [
            {
              key: 'Content-Type',
              value: 'application/json',
            },
          ],
          payload: {
            raw: {
              json: '{\n  "name":"Jose"\n}\n',
            },
          },
        },
        {
          method: 'GET',
          headers: [
            {
              key: 'Content-Type',
              value: 'application/json',
            },
          ],
          payload: {
            raw: {
              json: { name: 'Adam' },
            },
          },
        },
      ],
    };
    expect(VCM.resolve('testContext', input)).toEqual(output);
  });

  it('should load json variables into testContext', () => {
    const envVars = {
      user: 'Alice',
      age: 30,
      isAdmin: true,
      nested: { city: 'London' },
    };

    VCM.loadEnv('testContext', envVars);

    const context = VCM.createContext('testContext');
    expect(context).toBeDefined();
    expect(context?.getValue('user')).toBe('Alice');
    expect(context?.getValue('age')).toBe(30);
    expect(context?.getValue('isAdmin')).toBe(true);
    expect(context?.getValue('nested')).toEqual({ city: 'London' });
  });

  it('sets key-value pairs with { value, isSecret } format', () => {
    const input = {
      A: { value: 'secretVal', isSecret: true },
      B: { value: 42, isSecret: false },
    };

    VCM.loadEnv('testContext', input);
    const ctx = VCM.createContext('testContext');

    expect(ctx?.getValue('A')).toBe('secretVal');
    expect(ctx?.getValue('B')).toBe(42);
  });

  it('sets key-value pairs with { value, isSecret } format', () => {
    const input = {
      A: { value: 'secretVal', isSecret: true },
    };

    VCM.loadEnv('testContext', input);
    const ctx = VCM.createContext('testContext');

    expect(ctx?.getValue('A')).toBe('secretVal');
    expect(ctx?.getValue('B')).toBe(42);
  });

  it('should overwrite existing context if loadEnv called again with same contextId', () => {
    VCM.loadEnv('testContext', { user: 'Bob' });
    let context = VCM.createContext('testContext');
    expect(context?.getValue('user')).toBe('Bob');

    // Load again with different data
    VCM.loadEnv('testContext', { user: 'Carol', isAdmin: false });
    context = VCM.createContext('testContext');

    expect(context?.getValue('user')).toBe('Carol');
    expect(context?.getValue('isAdmin')).toBe(false);
    expect(context?.getValue('age')).toBe(30);

    context?.delete('age');
    expect(context?.getValue('age')).toBeUndefined();
  });
});
