/**
 * Copyright IBM Corp. 2024, 2025
 */
import axios from 'axios';
import { RestHandler } from '../../../src/engine/protocol/rest-handler.js';
import { AxiosClient } from '../../../src/engine/protocol/axios-client.js';
import { VCM } from '../../../src/engine/variable-context-manager/context-manager.js';
import { Request } from '../../../src/schemas/test.schema.js';
import { VariableContext } from '../../../src/engine/variable-context-manager/variable-context.js';
import fs from 'fs';

// Mock the LogWrapper
jest.mock('../../../src/service/log-wrapper.js', () => ({
  LogWrapper: {
    logWarn: jest.fn(),
    logInfo: jest.fn(),
    logError: jest.fn(),
    logDebug: jest.fn(),
  },
}));

jest.mock('axios');
const mockSetVariable = jest.fn();
const mockedAxios = axios as any as jest.Mock;
jest.mock('fs');

jest.spyOn(VCM, 'getContext').mockReturnValue({
  setVariable: mockSetVariable,
  set: mockSetVariable,
} as unknown as VariableContext);

describe('RestHandler with AxiosClient', () => {
  const sessionId = 'restHandlerContext';
  beforeAll(() => {
    VCM.createContext('restHandlerContext').setVariable('user', {
      id: '42',
      name: 'Bob',
    });
    VCM.createContext('restHandlerContext')?.setVariable(
      'token',
      'secrettoken',
    );
    VCM.createContext('restHandlerContext')?.setVariable('account', 'alice');
  });
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('should interpolate and make a GET request', async () => {
    mockedAxios.mockResolvedValueOnce({ data: { ok: true } });

    const handler = new RestHandler(new AxiosClient());

    const step: Request = {
      method: 'GET',
      resource: '${user.id}',
      endpoint: 'https://example.com/user/${user.id}',
      headers: [{ key: 'Authorization', value: 'Bearer ${token}' }],
    };

    const result = await handler.execute(step, sessionId);

    expect(mockedAxios).toHaveBeenCalledWith(
      expect.objectContaining({
        url: 'https://example.com/user/42',
        headers: expect.objectContaining({
          Authorization: 'Bearer secrettoken',
        }),
        method: 'GET',
      }),
    );
    expect(result.data).toEqual({ ok: true });
  });

  it('should throw error for missing variable when throwOnMissing is true', async () => {
    mockedAxios.mockResolvedValueOnce({ data: { success: true } });
    const handler = new RestHandler(new AxiosClient());

    const step: Request = {
      method: 'GET',
      resource: '${notfound}',
      endpoint: 'https://api.com/${notfound}',
    };

    const result = await handler.execute(step, sessionId);
    expect(mockedAxios).toHaveBeenCalledWith(
      expect.objectContaining({
        method: 'GET',
        url: 'https://api.com/',
      }),
    );
    expect(result.data).toEqual({ success: true });
  });

  it('should handle POST request with JSON body', async () => {
    mockedAxios.mockResolvedValueOnce({ data: { success: true } });

    const handler = new RestHandler(new AxiosClient());
    const step: Request = {
      method: 'POST',
      resource: 'create',
      endpoint: 'https://api.com/create',
      payload: { raw: { json: '{ name: "${user.name}" }' } },
      headers: [{ key: 'ContentType', value: 'application/json' }],
    };

    const result = await handler.execute(step, sessionId);
    expect(mockedAxios).toHaveBeenCalledWith(
      expect.objectContaining({
        method: 'POST',
        url: 'https://api.com/create',
        data: expect.stringContaining('{ name: "Bob" }'),
        headers: expect.objectContaining({
          ContentType: 'application/json',
        }),
      }),
    );
    expect(result.data).toEqual({ success: true });
  });

  it('should handle POST request with xml body', async () => {
    mockedAxios.mockResolvedValueOnce({ data: { success: true } });

    const handler = new RestHandler(new AxiosClient());
    const step: Request = {
      method: 'POST',
      resource: 'create',
      endpoint: 'https://api.com/create',
      payload: { raw: { xml: '{ name: "${user.name}" }' } },
      headers: [{ key: 'ContentType', value: 'application/xml' }],
    };

    const result = await handler.execute(step, sessionId);
    expect(mockedAxios).toHaveBeenCalledWith(
      expect.objectContaining({
        method: 'POST',
        url: 'https://api.com/create',
        data: expect.stringContaining('{ name: "Bob" }'),
        headers: expect.objectContaining({
          ContentType: 'application/xml',
        }),
      }),
    );
    expect(result.data).toEqual({ success: true });
  });

  it('should handle POST request with js body', async () => {
    mockedAxios.mockResolvedValueOnce({ data: { success: true } });

    const handler = new RestHandler(new AxiosClient());
    const step: Request = {
      method: 'POST',
      resource: 'create',
      endpoint: 'https://api.com/create',
      payload: { raw: { js: 'console.log("${user.name}")' } },
      headers: [{ key: 'ContentType', value: 'text/plain' }],
    };

    const result = await handler.execute(step, sessionId);
    expect(mockedAxios).toHaveBeenCalledWith(
      expect.objectContaining({
        method: 'POST',
        url: 'https://api.com/create',
        data: expect.stringContaining('console.log("Bob")'),
        headers: expect.objectContaining({
          ContentType: 'text/plain',
        }),
      }),
    );
    expect(result.data).toEqual({ success: true });
  });

  it('should handle POST request with html body', async () => {
    mockedAxios.mockResolvedValueOnce({ data: { success: true } });

    const handler = new RestHandler(new AxiosClient());
    const step: Request = {
      method: 'POST',
      resource: 'create',
      endpoint: 'https://api.com/create',
      payload: { raw: { js: '<html><body>${user.name}</body></html>' } },
      headers: [{ key: 'ContentType', value: 'text/plain' }],
    };

    const result = await handler.execute(step, sessionId);
    expect(mockedAxios).toHaveBeenCalledWith(
      expect.objectContaining({
        method: 'POST',
        url: 'https://api.com/create',
        data: expect.stringContaining('<html><body>Bob</body></html>'),
        headers: expect.objectContaining({
          ContentType: 'text/plain',
        }),
      }),
    );
    expect(result.data).toEqual({ success: true });
  });

  it('should send form-urlencoded data correctly', async () => {
    mockedAxios.mockResolvedValueOnce({ data: { status: 'ok' } });

    const handler = new RestHandler(new AxiosClient());
    const step: Request = {
      method: 'POST',
      resource: 'login',
      endpoint: 'https://api.com/login',
      payload: {
        urlEncodedFormData: [
          { key: 'username', value: 'test' },
          { key: 'password', value: '123' },
        ],
      },
      headers: [
        { key: 'ContentType', value: 'application/x-www-form-urlencoded' },
      ],
    };

    const result = await handler.execute(step, sessionId);

    expect(mockedAxios).toHaveBeenCalledWith(
      expect.objectContaining({
        headers: expect.objectContaining({
          ContentType: 'application/x-www-form-urlencoded',
        }),
        data: expect.stringContaining('username=test&password=123'), // pragma: allowlist secret
      }),
    );
    expect(result.data).toEqual({ status: 'ok' });
  });

  it('should handle XML response parsing', async () => {
    const xmlResponse = '<root><user>John</user><id>123</id></root>';
    mockedAxios.mockResolvedValueOnce({
      data: xmlResponse,
      headers: { 'content-type': 'application/xml' },
      status: 200,
      statusText: 'OK',
    });

    const handler = new RestHandler(new AxiosClient());
    const step: Request = {
      method: 'GET',
      endpoint: 'https://api.com/xml',
      resource: 'xml',
    };

    await handler.execute(step, sessionId);

    // Verify XML was parsed
    expect(mockSetVariable).toHaveBeenCalledWith(
      'xml()',
      expect.objectContaining({
        root: expect.objectContaining({
          user: 'John',
          id: '123',
        }),
      }),
    );
    expect(mockSetVariable).toHaveBeenCalledWith(
      'responseBody',
      expect.objectContaining({
        root: expect.objectContaining({
          user: 'John',
          id: '123',
        }),
      }),
    );
  });

  it('should handle file not found error in formData', async () => {
    // Mock fs.existsSync to simulate file not found
    (fs.existsSync as jest.Mock).mockReturnValue(false);

    // Create a custom error to be thrown when file is not found
    const fileNotFoundError = new Error(
      'File not found: /non/existent/path.txt',
    );

    // Mock fs.readFileSync to throw the error
    (fs.readFileSync as jest.Mock).mockImplementation(() => {
      throw fileNotFoundError;
    });

    const handler = new RestHandler(new AxiosClient());
    const step: Request = {
      method: 'POST',
      endpoint: 'https://api.com/upload',
      resource: 'upload',
      payload: {
        formData: [
          { key: 'file', value: '/non/existent/path.txt', type: 'file' },
        ],
      },
    };

    // Clear previous mock calls
    mockSetVariable.mockClear();

    // Execute and expect it to throw
    await expect(handler.execute(step, sessionId)).rejects.toThrow();

    // Check that the error response was set in the context
    expect(mockSetVariable).toHaveBeenCalledWith('request', expect.anything());

    // The error response should be set at some point
    const calls = mockSetVariable.mock.calls;
    const responseCall = calls.find((call) => call[0] === 'response');

    expect(responseCall).toBeDefined();
    if (responseCall) {
      const responseArg = responseCall[1];
      expect(responseArg).toEqual(undefined);
    }
  });

  it('should handle variable resolution errors', async () => {
    // Mock VCM.resolve to throw an error
    jest.spyOn(VCM, 'resolve').mockImplementationOnce(() => {
      throw new Error('Variable not found: ${missing}');
    });

    const handler = new RestHandler(new AxiosClient());
    const step: Request = {
      method: 'GET',
      endpoint: 'https://api.com/${missing}',
      resource: 'error',
    };

    await expect(handler.execute(step, sessionId)).rejects.toThrow(
      'Variable not found: ${missing}',
    );

    // Verify error response is set in context
    expect(mockSetVariable).toHaveBeenCalledWith(
      'response',
      expect.objectContaining({
        status: 0,
        statusText: 'Variable Resolution Error',
        data: expect.objectContaining({
          error: 'Variable not found: ${missing}',
        }),
      }),
    );
  });

  it('should handle HTTP request failures', async () => {
    // Mock axios to throw an error with a response
    const errorResponse = {
      status: 500,
      statusText: 'Internal Server Error',
      headers: { 'content-type': 'application/json' },
      data: { error: 'Server error' },
    };

    const error = new Error('Request failed');
    (error as any).response = errorResponse;

    mockedAxios.mockRejectedValueOnce(error);

    const handler = new RestHandler(new AxiosClient());
    const step: Request = {
      method: 'GET',
      endpoint: 'https://api.com/error',
      resource: 'error',
    };

    await expect(handler.execute(step, sessionId)).rejects.toThrow(
      'Request failed',
    );

    // Verify error response is set in context
    expect(mockSetVariable).toHaveBeenCalledWith('response', errorResponse);
    expect(mockSetVariable).toHaveBeenCalledWith('responseStatus', 500);
  });

  it('should handle HTTP request failures without response object', async () => {
    // Mock axios to throw an error without a response
    const error = new Error('Network error');

    mockedAxios.mockRejectedValueOnce(error);

    const handler = new RestHandler(new AxiosClient());
    const step: Request = {
      method: 'GET',
      endpoint: 'https://api.com/network-error',
      resource: 'error',
    };

    await expect(handler.execute(step, sessionId)).rejects.toThrow(
      'Network error',
    );

    // Verify error itself is set as response in context
    expect(mockSetVariable).toHaveBeenCalledWith('response', error);
  });

  it('should handle SSL verification settings', async () => {
    mockedAxios.mockResolvedValueOnce({ data: { success: true } });

    const handler = new RestHandler(new AxiosClient());
    const step: Request = {
      method: 'GET',
      endpoint: 'https://api.com/secure',
      resource: 'secure',
      settings: {
        sslVerification: false,
      },
    };

    await handler.execute(step, sessionId);

    expect(mockedAxios).toHaveBeenCalledWith(
      expect.objectContaining({
        httpsAgent: expect.objectContaining({
          options: expect.objectContaining({
            rejectUnauthorized: false,
          }),
        }),
      }),
    );
  });

  it('should attach basic auth headers', async () => {
    mockedAxios.mockResolvedValueOnce({ data: { auth: true } });

    const handler = new RestHandler(new AxiosClient());
    const step: Request = {
      method: 'GET',
      resource: 'secure',
      endpoint: 'https://api.com/secure',
      auth: {
        basicAuth: {
          username: 'foo',
          password: '<PASSWORD>', // pragma: allowlist secret
        },
      },
    };

    const result = await handler.execute(step, sessionId);

    expect(mockedAxios).toHaveBeenCalledWith(
      expect.objectContaining({
        headers: expect.objectContaining({
          Authorization: 'Basic Zm9vOjxQQVNTV09SRD4=',
        }),
      }),
    );
    expect(result.data).toEqual({ auth: true });
  });

  it('should interpolate nested headers and query params', async () => {
    mockedAxios.mockResolvedValueOnce({ data: { query: true } });

    const handler = new RestHandler(new AxiosClient());
    const step: Request = {
      method: 'GET',
      resource: 'search',
      endpoint: 'https://api.com/search',
      headers: [{ key: 'X-User', value: '${user.name}' }],
      parameters: [
        { key: 'id', value: '${user.id}' },
        { key: 'name', value: '${user.name}' },
      ],
    };

    const result = await handler.execute(step, sessionId);

    expect(mockedAxios).toHaveBeenCalledWith(
      expect.objectContaining({
        params: { id: '42', name: 'Bob' },
        headers: expect.objectContaining({ 'X-User': 'Bob' }),
      }),
    );
    expect(result.data).toEqual({ query: true });
  });

  it('should override default headers with interpolated values', async () => {
    mockedAxios.mockResolvedValueOnce({ data: { overridden: true } });

    const handler = new RestHandler(new AxiosClient());
    const step: Request = {
      method: 'GET',
      resource: 'data',
      endpoint: 'https://api.com/data',
      auth: {
        bearerToken: '${token}',
      },
      headers: [{ key: 'Content-Type', value: 'application/json' }],
    };

    const result = await handler.execute(step, sessionId);

    expect(mockedAxios).toHaveBeenCalledWith(
      expect.objectContaining({
        headers: expect.objectContaining({
          Authorization: 'Bearer secrettoken',
          'Content-Type': 'application/json',
        }),
      }),
    );
    expect(result.data).toEqual({ overridden: true });
  });

  it('should interpolate each item in an array', () => {
    const arr = ['User ID: ${user.id}', 'Token: ${token}'];

    const result = VCM.resolve('restHandlerContext', arr);

    expect(result).toEqual(['User ID: 42', 'Token: secrettoken']);
  });

  it('should throw error if HTTP url is missing', async () => {
    const handler = new RestHandler(new AxiosClient());

    const step: Request = {
      method: 'GET',
      resource: 'nomethod',
      // enpoint is missing here
    };

    await expect(handler.execute(step, sessionId)).rejects.toThrow(
      'Endpoint is required',
    );
  });

  it('should handle empty payload without throwing', async () => {
    mockedAxios.mockResolvedValueOnce({ data: { ok: true } });

    const handler = new RestHandler(new AxiosClient());
    const step: Request = {
      method: 'POST',
      resource: 'empty',
      endpoint: 'https://api.com',
      payload: undefined,
    };

    const result = await handler.execute(step, sessionId);

    expect(mockedAxios).toHaveBeenCalledWith(
      expect.objectContaining({
        method: 'POST',
      }),
    );
    expect(result.data).toEqual({ ok: true });
  });

  it('should send multipart/form-data using FormData payload', async () => {
    mockedAxios.mockResolvedValueOnce({ data: { uploaded: true } });

    const handler = new RestHandler(new AxiosClient());
    const step: Request = {
      method: 'POST',
      endpoint: 'https://api.com',
      resource: 'upload',
      headers: [{ key: 'ContentType', value: 'application/json' }],
      payload: {
        formData: [
          { key: 'file', value: 'mock-file-content' },
          { key: 'description', value: 'test upload' },
        ],
      },
    };

    const result = await handler.execute(step, sessionId);
    expect(mockedAxios).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.any(FormData),
      }),
    );
    expect(result.data).toEqual({ uploaded: true });
  });

  it('should handle file uploads in FormData', async () => {
    // Clear previous mock calls
    mockSetVariable.mockClear();
    mockedAxios.mockClear();

    // Mock file existence check
    (fs.existsSync as jest.Mock).mockReturnValue(true);

    // Mock file reading
    (fs.readFileSync as jest.Mock).mockReturnValue(
      Buffer.from('test file content'),
    );
    // Mock axios response
    mockedAxios.mockResolvedValueOnce({
      data: { fileUploaded: true },
      status: 200,
      statusText: 'OK',
      headers: {},
    });

    const handler = new RestHandler(new AxiosClient());
    const step: Request = {
      method: 'POST',
      endpoint: 'https://api.com/upload',
      resource: 'upload',
      payload: {
        formData: [
          { key: 'file', value: '/path/to/test.txt', type: 'file' },
          { key: 'description', value: 'File upload test' },
        ],
      },
    };

    const result = await handler.execute(step, sessionId);

    // Verify the response
    expect(result.data).toEqual({ fileUploaded: true });

    // Verify that the request was made with FormData
    expect(mockedAxios).toHaveBeenCalledWith(
      expect.objectContaining({
        method: 'POST',
        url: 'https://api.com/upload',
      }),
    );

    // Verify that response variables were set
    expect(mockSetVariable).toHaveBeenCalledWith('responseStatus', 200);
    expect(mockSetVariable).toHaveBeenCalledWith('responseBody', {
      fileUploaded: true,
    });
  });

  it('should store selected keys in context when step.var is an array', async () => {
    const userId = 123;
    const address = {
      street: '123 Main St',
      city: 'New York',
      state: 'NY',
    };
    const address1 = {
      street: '456 Elm St',
      city: 'Florida',
      state: 'US',
    };
    const address2 = {
      street: '789 Oak St',
      city: 'Florida',
      state: 'FL',
    };
    const user = {
      userId,
      name: 'Bob',
      address: [address, address1],
    };
    const response = [
      user,
      {
        userId: 456,
        name: 'Alice',
        address: [address2],
      },
    ];
    mockedAxios.mockResolvedValueOnce({
      data: response,
    });

    const handler = new RestHandler(new AxiosClient());
    const step: Request = {
      method: 'GET',
      endpoint: 'https://api.com',
      resource: 'users',
      var: [
        { id: 'userId' },
        { userId: '[0].userId' },
        { address: '[1].address' },
        { firstAddress: '[0].address[0]' },
        { response1: '[0]' },
        { response1_name: '[0].name' },
        { response1_address2: '[0].address[1]' },
      ],
    };

    const result = await handler.execute(step, sessionId);
    expect(mockSetVariable).toHaveBeenCalledWith('id', undefined);
    expect(mockSetVariable).toHaveBeenCalledWith('userId', userId);
    expect(mockSetVariable).toHaveBeenCalledWith('address', [address2]);
    expect(mockSetVariable).toHaveBeenCalledWith('firstAddress', address);
    expect(mockSetVariable).toHaveBeenCalledWith('response1', user);
    expect(mockSetVariable).toHaveBeenCalledWith('response1_name', user.name);
    expect(mockSetVariable).toHaveBeenCalledWith(
      'response1_address2',
      address1,
    );
    expect(result).toMatchObject(expect.objectContaining({ data: response }));
  });

  it('should store full response in context when step.var is a string', async () => {
    mockedAxios.mockResolvedValueOnce({
      data: { token: 'secrettoken', user: 'alice' },
    });
    const handler = new RestHandler(new AxiosClient());

    const step: Request = {
      method: 'GET',
      endpoint: 'https://api.com',
      resource: 'login',
      var: 'loginResponse',
    };
    const result = await handler.execute(step, sessionId);
    expect(mockSetVariable).toHaveBeenCalledWith(
      'loginResponse',
      expect.objectContaining({ token: 'secrettoken', user: 'alice' }),
    );
    expect(result.data).toEqual({ token: 'secrettoken', user: 'alice' });
  });

  it('should handle system variables with undefined values', async () => {
    mockedAxios.mockResolvedValueOnce({ data: { success: true } });

    // Mock VCM.getContext to return a context with specific behavior for system variables
    const mockContext = {
      set: mockSetVariable,
      get: jest.fn().mockImplementation((key) => {
        if (key === 'responseBody') {
          return undefined; // Known system var with undefined value
        }
        return null;
      }),
    };
    jest.spyOn(VCM, 'getContext').mockReturnValue(mockContext as any);

    const handler = new RestHandler(new AxiosClient());
    const step: Request = {
      method: 'GET',
      endpoint: 'https://api.com/test',
      resource: 'test',
      var: [
        { key: 'responseValue', value: 'responseBody.data' },
        { key: 'unknownValue', value: '__unknown_var__.data' },
      ],
    };

    await handler.execute(step, sessionId);

    // Verify that the logger was used for both cases
    expect(mockSetVariable).toHaveBeenCalledWith('responseValue', undefined);
    expect(mockSetVariable).toHaveBeenCalledWith('unknownValue', undefined);
  });
});
