// Copyright 2025 Google LLC
//
// 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 {ToolboxClient} from '../src/toolbox_core/client';
import {ToolboxTool} from '../src/toolbox_core/tool';
import {
  ZodManifestSchema,
  createZodSchemaFromParams,
  type ZodManifest,
  ZodToolSchema,
  type ParameterSchema,
} from '../src/toolbox_core/protocol';
import axios, {AxiosInstance, AxiosResponse} from 'axios';
import {z, ZodRawShape, ZodObject, ZodTypeAny, ZodError} from 'zod';

// --- Helper Types ---
type OriginalToolboxToolType =
  typeof import('../src/toolbox_core/tool').ToolboxTool;

type CallableToolReturnedByFactory = ReturnType<OriginalToolboxToolType>;

type InferredZodTool = z.infer<typeof ZodToolSchema>;

const createMockZodObject = (shape: ZodRawShape = {}): ZodObject<ZodRawShape> =>
  ({
    parse: jest.fn(args => args),
    _def: {
      typeName: 'ZodObject',
      shape: () => shape,
    },
    shape: shape,
    pick: jest.fn().mockReturnThis(),
    omit: jest.fn().mockReturnThis(),
    extend: jest.fn().mockReturnThis(),
  }) as unknown as ZodObject<ZodRawShape>;

// --- Mocking External Dependencies ---
jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;

jest.mock('../src/toolbox_core/tool', () => ({
  ToolboxTool: jest.fn(),
}));

const MockedToolboxToolFactory =
  ToolboxTool as jest.MockedFunction<OriginalToolboxToolType>;

// This mock setup is from the user's baseline
jest.mock('../src/toolbox_core/protocol', () => {
  const actualProtocol = jest.requireActual('../src/toolbox_core/protocol');
  return {
    ...actualProtocol,
    ZodManifestSchema: {
      ...actualProtocol.ZodManifestSchema, // Preserve other schema properties
      parse: jest.fn(),
    },
    createZodSchemaFromParams: jest.fn(),
  };
});
const MockedZodManifestSchema = ZodManifestSchema as jest.Mocked<
  typeof ZodManifestSchema
>;
const MockedCreateZodSchemaFromParams =
  createZodSchemaFromParams as jest.MockedFunction<
    typeof createZodSchemaFromParams
  >;

describe('ToolboxClient', () => {
  const testBaseUrl = 'http://api.example.com';
  let consoleErrorSpy: jest.SpyInstance;
  let mockSessionGet: jest.Mock;
  let autoCreatedSession: AxiosInstance;

  beforeEach(() => {
    jest.resetAllMocks();

    mockSessionGet = jest.fn();
    autoCreatedSession = {
      get: mockSessionGet,
      post: jest.fn(),
      defaults: {headers: {} as import('axios').HeadersDefaults},
      interceptors: {
        request: {use: jest.fn()},
        response: {use: jest.fn()},
      } as unknown as import('axios').AxiosInstance['interceptors'],
    } as unknown as AxiosInstance;
    mockedAxios.create.mockReturnValue(autoCreatedSession);

    consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
  });

  afterEach(() => {
    consoleErrorSpy.mockRestore();
  });

  describe('constructor', () => {
    it('should set baseUrl and create a new session if one is not provided', () => {
      new ToolboxClient(testBaseUrl);

      expect(mockedAxios.create).toHaveBeenCalledTimes(1);
      expect(mockedAxios.create).toHaveBeenCalledWith({baseURL: testBaseUrl});
    });

    it('should set baseUrl and use the provided session if one is given', () => {
      const customMockSession = {
        get: mockSessionGet,
      } as unknown as AxiosInstance;
      new ToolboxClient(testBaseUrl, customMockSession);
      expect(mockedAxios.create).not.toHaveBeenCalled();
    });
  });

  describe('loadTool', () => {
    const toolName = 'calculator';
    const expectedApiUrl = `${testBaseUrl}/api/tool/${toolName}`;
    let client: ToolboxClient;

    beforeEach(() => {
      client = new ToolboxClient(testBaseUrl);
    });

    const setupMocksForSuccessfulLoad = (
      toolDefinition: {
        // This is the original generic object type for loadTool
        description: string;
        parameters: {
          name: string;
          type: string;
          description: string;
          authSources?: string[];
        }[];
        authRequired?: string[];
      },
      overrides: {
        manifestData?: Partial<ZodManifest>;
        zodParamsSchema?: ZodObject<ZodRawShape>;
        toolInstance?: Partial<CallableToolReturnedByFactory>;
      } = {},
    ) => {
      const manifestData: ZodManifest = {
        serverVersion: '1.0.0',
        tools: {[toolName]: toolDefinition as unknown as InferredZodTool},
        ...overrides.manifestData,
      } as ZodManifest; // Outer cast to ZodManifest

      const zodParamsSchema =
        overrides.zodParamsSchema ||
        createMockZodObject(
          (toolDefinition.parameters as unknown as ParameterSchema[]).reduce(
            (shapeAccumulator: {[k: string]: ZodTypeAny}, param) => {
              if (!param.authSources) {
                shapeAccumulator[param.name] = {
                  _def: {typeName: 'ZodString'},
                } as unknown as ZodTypeAny;
              }
              return shapeAccumulator;
            },
            {},
          ),
        );

      const defaultMockCallable = jest
        .fn()
        .mockResolvedValue({result: 'mock tool execution'});
      const defaultToolInstance: CallableToolReturnedByFactory = Object.assign(
        defaultMockCallable,
        {
          toolName: toolName,
          description: toolDefinition.description,
          params: zodParamsSchema,
          getName: jest.fn().mockReturnValue(toolName),
          getDescription: jest.fn().mockReturnValue(toolDefinition.description),
          getParamSchema: jest.fn().mockReturnValue(zodParamsSchema),
          boundParams: {},
          bindParams: jest.fn().mockReturnThis(),
          bindParam: jest.fn().mockReturnThis(),
          authTokenGetters: {},
          requiredAuthnParams: {},
          requiredAuthzTokens: [],
          clientHeaders: {},
          toJSON: jest.fn(() => ({})),
          withAuth: jest.fn().mockReturnThis(),
          addAuthTokenGetters: jest.fn().mockReturnThis(),
          addAuthTokenGetter: jest.fn().mockReturnThis(),
        },
      );

      const toolInstance = overrides.toolInstance
        ? {...defaultToolInstance, ...overrides.toolInstance}
        : defaultToolInstance;

      mockSessionGet.mockResolvedValueOnce({
        data: manifestData,
      } as AxiosResponse);
      MockedZodManifestSchema.parse.mockReturnValueOnce(manifestData);
      MockedCreateZodSchemaFromParams.mockReturnValueOnce(zodParamsSchema);

      MockedToolboxToolFactory.mockReturnValueOnce(
        toolInstance as CallableToolReturnedByFactory,
      );

      return {manifestData, zodParamsSchema, toolInstance};
    };

    it('should successfully load a tool with valid manifest and API response', async () => {
      const mockToolDefinition = {
        // Original generic object
        description: 'Performs calculations',
        parameters: [
          {name: 'expression', type: 'string', description: 'Math expression'},
        ],
      };

      const {zodParamsSchema, toolInstance, manifestData} =
        setupMocksForSuccessfulLoad(mockToolDefinition);
      const loadedTool = await client.loadTool(toolName);

      expect(mockSessionGet).toHaveBeenCalledWith(
        expectedApiUrl,
        expect.any(Object),
      );
      expect(MockedZodManifestSchema.parse).toHaveBeenCalledWith(manifestData);
      expect(MockedCreateZodSchemaFromParams).toHaveBeenCalledWith(
        mockToolDefinition.parameters as unknown as ParameterSchema[], // Cast if createZodSchemaFromParams expects ParameterSchema[]
      );
      expect(MockedToolboxToolFactory).toHaveBeenCalledWith(
        autoCreatedSession,
        testBaseUrl,
        toolName,
        mockToolDefinition.description,
        zodParamsSchema,
        {}, // authTokenGetters
        {}, // remainingAuthnParams
        [], // remainingAuthzTokens
        {}, // currBoundParams
        {}, // clientHeaders
      );
      expect(loadedTool).toBe(toolInstance);
    });

    it('should successfully load a tool with valid bound parameters', async () => {
      const mockToolDefinition = {
        description: 'Performs calculations',
        parameters: [
          {name: 'expression', type: 'string', description: 'Math expression'},
          {name: 'precision', type: 'number', description: 'Decimal places'},
        ],
      };
      const boundParams = {expression: '2+2'};
      setupMocksForSuccessfulLoad(mockToolDefinition);

      await client.loadTool(toolName, {}, boundParams);

      // Assert that the factory was called with the applicable bound parameters
      expect(MockedToolboxToolFactory).toHaveBeenCalledWith(
        autoCreatedSession,
        expect.anything(),
        expect.anything(),
        expect.anything(),
        expect.anything(),
        {}, // authTokenGetters
        expect.anything(),
        expect.anything(),
        boundParams, // currBoundParams
        expect.anything(),
      );
    });

    it('should throw an error if unused bound parameters are provided', async () => {
      const mockToolDefinition = {
        description: 'A tool',
        parameters: [{name: 'param1', type: 'string', description: 'A param'}],
      };
      const boundParams = {param1: 'value1', unusedParam: 'value2'};
      setupMocksForSuccessfulLoad(mockToolDefinition);

      await expect(client.loadTool(toolName, {}, boundParams)).rejects.toThrow(
        `Validation failed for tool '${toolName}': unused bound parameters: unusedParam.`,
      );
    });

    it('should throw an error if manifest parsing fails', async () => {
      const mockApiResponseData = {invalid: 'manifest structure'};
      const mockZodError = new ZodError([
        {
          input: undefined,
          path: ['tools'],
          message: 'Required',
          code: 'invalid_type',
          expected: 'object',
        },
      ]);

      mockSessionGet.mockResolvedValueOnce({
        data: mockApiResponseData,
      } as AxiosResponse);
      MockedZodManifestSchema.parse.mockImplementationOnce(() => {
        throw mockZodError;
      });

      await expect(client.loadTool(toolName)).rejects.toThrow(
        `Invalid manifest structure received from ${expectedApiUrl}: ${JSON.stringify(mockZodError.issues, null, 2)}`,
      );
      expect(MockedCreateZodSchemaFromParams).not.toHaveBeenCalled();
      expect(MockedToolboxToolFactory).not.toHaveBeenCalled();
    });

    it('should throw an error if manifest parsing fails with a non-ZodError', async () => {
      const genericError = new Error('A generic parsing error');
      mockSessionGet.mockResolvedValueOnce({data: {}});
      MockedZodManifestSchema.parse.mockImplementationOnce(() => {
        throw genericError;
      });

      await expect(client.loadTool(toolName)).rejects.toThrow(
        `Invalid manifest structure received from ${expectedApiUrl}: ${genericError.message}`,
      );
    });

    it('should throw an error if manifest parsing fails with a non-Error object', async () => {
      const nonError = 'a string error';
      mockSessionGet.mockResolvedValueOnce({data: {}});
      MockedZodManifestSchema.parse.mockImplementationOnce(() => {
        throw nonError;
      });

      await expect(client.loadTool(toolName)).rejects.toThrow(
        `Invalid manifest structure received from ${expectedApiUrl}: Unknown validation error.`,
      );
    });

    it('should throw an error if manifest.tools key is missing', async () => {
      const mockManifestWithoutTools = {
        serverVersion: '1.0.0',
        tools: undefined,
      };

      mockSessionGet.mockResolvedValueOnce({
        data: mockManifestWithoutTools,
      } as AxiosResponse);
      MockedZodManifestSchema.parse.mockReturnValueOnce(
        mockManifestWithoutTools as unknown as ZodManifest,
      );

      await expect(client.loadTool(toolName)).rejects.toThrow(
        `Tool "${toolName}" not found in manifest from /api/tool/${toolName}.`,
      );
      expect(MockedCreateZodSchemaFromParams).not.toHaveBeenCalled();
      expect(MockedToolboxToolFactory).not.toHaveBeenCalled();
    });

    it('should throw an error if the specific tool is not found in manifest.tools', async () => {
      const mockManifestWithOtherTools = {
        serverVersion: '1.0.0',
        tools: {anotherTool: {description: 'A different tool', parameters: []}}, // Kept generic as per baseline
      } as ZodManifest;
      mockSessionGet.mockResolvedValueOnce({
        data: mockManifestWithOtherTools,
      } as AxiosResponse);
      MockedZodManifestSchema.parse.mockReturnValueOnce(
        mockManifestWithOtherTools,
      );
      await expect(client.loadTool(toolName)).rejects.toThrow(
        `Tool "${toolName}" not found in manifest from /api/tool/${toolName}.`,
      );
      expect(MockedCreateZodSchemaFromParams).not.toHaveBeenCalled();
      expect(MockedToolboxToolFactory).not.toHaveBeenCalled();
    });

    it('should throw and log error if API GET request fails', async () => {
      const apiError = new Error('Server-side issue');
      mockSessionGet.mockRejectedValueOnce(apiError);

      await expect(client.loadTool(toolName)).rejects.toThrow(apiError);
      expect(mockSessionGet).toHaveBeenCalledWith(
        expectedApiUrl,
        expect.any(Object),
      );
      expect(consoleErrorSpy).toHaveBeenCalledWith(
        `Error fetching data from ${expectedApiUrl}:`,
        apiError.message,
      );
      expect(MockedZodManifestSchema.parse).not.toHaveBeenCalled();
    });

    it('should resolve and pass client headers to the request', async () => {
      const clientHeaders = {
        'X-Static-Header': 'static-value',
        'X-Dynamic-Header': () => Promise.resolve('dynamic-value'),
      };
      client = new ToolboxClient(testBaseUrl, null, clientHeaders);
      const mockToolDefinition = {
        description: 'A tool',
        parameters: [],
      };
      setupMocksForSuccessfulLoad(mockToolDefinition);
      await client.loadTool(toolName);
      expect(mockSessionGet).toHaveBeenCalledWith(expectedApiUrl, {
        headers: {
          'X-Static-Header': 'static-value',
          'X-Dynamic-Header': 'dynamic-value',
        },
      });
    });

    it('should successfully load a tool with authentication requirements', async () => {
      const mockToolDefinition = {
        description: 'An authenticated tool',
        parameters: [
          {
            name: 'user_token',
            type: 'string',
            description: 'User auth token',
            authSources: ['UserService'],
          },
        ],
        authRequired: [],
      };
      const authTokenGetters = {UserService: () => 'secret-user-token'};
      setupMocksForSuccessfulLoad(mockToolDefinition);

      await client.loadTool(toolName, authTokenGetters);

      expect(MockedCreateZodSchemaFromParams).toHaveBeenCalledWith([]);
      expect(MockedToolboxToolFactory).toHaveBeenCalledWith(
        expect.anything(),
        expect.anything(),
        expect.anything(),
        expect.anything(),
        expect.anything(),
        authTokenGetters,
        {},
        [],
        expect.anything(),
        expect.anything(),
      );
    });

    it('should throw an error if unused auth tokens are provided', async () => {
      const mockToolDefinition = {
        description: 'A tool with no auth',
        parameters: [{name: 'param1', type: 'string', description: 'A param'}],
      };
      const authTokenGetters = {UnusedService: () => 'some-token'};
      setupMocksForSuccessfulLoad(mockToolDefinition);

      await expect(client.loadTool(toolName, authTokenGetters)).rejects.toThrow(
        `Validation failed for tool '${toolName}': unused auth tokens: UnusedService.`,
      );
    });
  });

  // --- loadToolset Tests ---
  describe('loadToolset', () => {
    let client: ToolboxClient;

    beforeEach(() => {
      client = new ToolboxClient(testBaseUrl);
    });

    const setupMocksForSuccessfulToolsetLoad = (
      toolDefinitions: Record<string, InferredZodTool>, // Use InferredZodTool
      manifestDataOverride?: ZodManifest,
    ) => {
      const manifestData: ZodManifest = manifestDataOverride || {
        serverVersion: '1.0.0',
        tools: toolDefinitions,
      };

      const zodParamsSchemas: Record<string, ZodObject<ZodRawShape>> = {};
      const toolInstances: Record<string, CallableToolReturnedByFactory> = {};
      const orderedToolNames = Object.keys(toolDefinitions);

      orderedToolNames.forEach(tName => {
        const tDef = toolDefinitions[tName];
        zodParamsSchemas[tName] = createMockZodObject(
          (tDef.parameters as ParameterSchema[]).reduce(
            (acc: {[k: string]: ZodTypeAny}, p) => {
              acc[p.name] = {
                _def: {typeName: 'ZodString'},
              } as unknown as ZodTypeAny;
              return acc;
            },
            {},
          ),
        );

        const mockCallable = jest
          .fn()
          .mockResolvedValue({result: `${tName} executed`});
        toolInstances[tName] = Object.assign(mockCallable, {
          toolName: tName,
          description: tDef.description,
          params: zodParamsSchemas[tName],
          getName: jest.fn().mockReturnValue(tName),
          getDescription: jest.fn().mockReturnValue(tDef.description),
          getParamSchema: jest.fn().mockReturnValue(zodParamsSchemas[tName]),
          boundParams: {},
          bindParams: jest.fn().mockReturnThis(),
          bindParam: jest.fn().mockReturnThis(),
          authTokenGetters: {},
          requiredAuthnParams: {},
          requiredAuthzTokens: [],
          clientHeaders: {},
          toJSON: jest.fn(() => ({})),
          withAuth: jest.fn().mockReturnThis(),
          addAuthTokenGetters: jest.fn().mockReturnThis(),
          addAuthTokenGetter: jest.fn().mockReturnThis(),
        });
      });

      mockSessionGet.mockResolvedValueOnce({
        data: manifestData,
      } as AxiosResponse);
      MockedZodManifestSchema.parse.mockReturnValueOnce(manifestData);

      orderedToolNames.forEach(tName => {
        MockedCreateZodSchemaFromParams.mockReturnValueOnce(
          zodParamsSchemas[tName],
        );
      });

      let factoryCallCount = 0;
      MockedToolboxToolFactory.mockImplementation(() => {
        const currentToolName = orderedToolNames[factoryCallCount];
        factoryCallCount++;
        if (currentToolName && toolInstances[currentToolName]) {
          return toolInstances[currentToolName];
        }
        const fallbackCallable = jest.fn();
        return Object.assign(fallbackCallable, {
          toolName: 'fallback',
        }) as unknown as CallableToolReturnedByFactory;
      });

      return {manifestData, zodParamsSchemas, toolInstances};
    };

    it('should successfully load a toolset with multiple tools', async () => {
      const toolsetName = 'my-toolset';
      const expectedApiUrl = `${testBaseUrl}/api/toolset/${toolsetName}`;
      const mockToolDefinitions: Record<string, InferredZodTool> = {
        toolA: {
          description: 'Tool A description',
          parameters: [
            {
              name: 'paramA',
              type: 'string',
              description: 'Param A',
            } as ParameterSchema,
          ],
          authRequired: [], // Assuming InferredZodTool might have this
        },
        toolB: {
          description: 'Tool B description',
          parameters: [
            {
              name: 'paramB',
              type: 'integer',
              description: 'Param B',
            } as ParameterSchema,
          ],
          authRequired: [], // Assuming InferredZodTool might have this
        },
      };

      const {toolInstances, manifestData, zodParamsSchemas} =
        setupMocksForSuccessfulToolsetLoad(mockToolDefinitions);
      const loadedTools = await client.loadToolset(toolsetName);

      expect(mockSessionGet).toHaveBeenCalledWith(
        expectedApiUrl,
        expect.any(Object),
      );
      expect(MockedZodManifestSchema.parse).toHaveBeenCalledWith(manifestData);

      expect(MockedCreateZodSchemaFromParams).toHaveBeenCalledWith(
        mockToolDefinitions.toolA.parameters,
      );
      expect(MockedCreateZodSchemaFromParams).toHaveBeenCalledWith(
        mockToolDefinitions.toolB.parameters,
      );
      expect(MockedToolboxToolFactory).toHaveBeenCalledTimes(2);
      expect(MockedToolboxToolFactory).toHaveBeenCalledWith(
        autoCreatedSession,
        testBaseUrl,
        'toolA',
        mockToolDefinitions.toolA.description,
        zodParamsSchemas.toolA,
        {},
        {},
        [],
        {},
        {},
      );
      expect(MockedToolboxToolFactory).toHaveBeenCalledWith(
        autoCreatedSession,
        testBaseUrl,
        'toolB',
        mockToolDefinitions.toolB.description,
        zodParamsSchemas.toolB,
        {},
        {},
        [],
        {},
        {},
      );
      expect(loadedTools).toEqual(
        expect.arrayContaining([toolInstances.toolA, toolInstances.toolB]),
      );
      expect(loadedTools.length).toBe(2);
    });

    it('should successfully load a toolset with bound parameters applicable to its tools', async () => {
      const toolsetName = 'my-toolset';
      const mockToolDefinitions: Record<string, InferredZodTool> = {
        toolA: {
          description: 'Tool A',
          parameters: [{name: 'paramA', type: 'string'} as ParameterSchema],
        },
        toolB: {
          description: 'Tool B',
          parameters: [{name: 'paramB', type: 'integer'} as ParameterSchema],
        },
      };
      const boundParams = {paramA: 'valueA', paramB: 123};

      setupMocksForSuccessfulToolsetLoad(mockToolDefinitions);
      await client.loadToolset(toolsetName, {}, boundParams);

      expect(MockedToolboxToolFactory).toHaveBeenCalledTimes(2);
      expect(MockedToolboxToolFactory).toHaveBeenCalledWith(
        autoCreatedSession,
        expect.anything(),
        'toolA',
        expect.anything(),
        expect.anything(),
        expect.anything(),
        expect.anything(),
        expect.anything(),
        {paramA: 'valueA'}, // Correct bound params for toolA
        expect.anything(),
      );
      expect(MockedToolboxToolFactory).toHaveBeenCalledWith(
        autoCreatedSession,
        expect.anything(),
        'toolB',
        expect.anything(),
        expect.anything(),
        expect.anything(),
        expect.anything(),
        expect.anything(),
        {paramB: 123}, // Correct bound params for toolB
        expect.anything(),
      );
    });

    it('should throw an error if bound parameters cannot be applied to any tool in the set', async () => {
      const toolsetName = 'my-toolset';
      const mockToolDefinitions: Record<string, InferredZodTool> = {
        toolA: {
          description: 'Tool A',
          parameters: [{name: 'paramA', type: 'string'} as ParameterSchema],
        },
      };
      const boundParams = {paramA: 'valueA', unusedParam: 'value2'};

      setupMocksForSuccessfulToolsetLoad(mockToolDefinitions);

      await expect(
        client.loadToolset(toolsetName, {}, boundParams),
      ).rejects.toThrow(
        "Validation failed for toolset 'my-toolset': unused bound parameters could not be applied to any tool: unusedParam.",
      );
    });

    it('should throw an error if auth tokens cannot be applied to any tool in the set', async () => {
      const toolsetName = 'my-toolset';
      const mockToolDefinitions: Record<string, InferredZodTool> = {
        toolA: {
          description: 'Tool A',
          parameters: [{name: 'paramA', type: 'string'} as ParameterSchema],
        },
      };
      const authTokenGetters = {UnusedService: () => 'token'};
      setupMocksForSuccessfulToolsetLoad(mockToolDefinitions);

      await expect(
        client.loadToolset(toolsetName, authTokenGetters),
      ).rejects.toThrow(
        "Validation failed for toolset 'my-toolset': unused auth tokens could not be applied to any tool: UnusedService.",
      );
    });

    it('should request the default toolset if no name is provided', async () => {
      const expectedApiUrl = `${testBaseUrl}/api/toolset/`;

      setupMocksForSuccessfulToolsetLoad({});
      await client.loadToolset();
      expect(mockSessionGet).toHaveBeenLastCalledWith(
        expectedApiUrl,
        expect.any(Object),
      );

      jest.clearAllMocks();
      mockedAxios.create.mockReturnValue(autoCreatedSession);

      setupMocksForSuccessfulToolsetLoad({});
      await client.loadToolset(undefined);
      expect(mockSessionGet).toHaveBeenLastCalledWith(
        expectedApiUrl,
        expect.any(Object),
      );
    });

    it('should return an empty array if the manifest contains no tools', async () => {
      const toolsetName = 'empty-set';
      const manifestWithNoTools: ZodManifest = {
        serverVersion: '1.0.0',
        tools: {},
      };
      setupMocksForSuccessfulToolsetLoad({}, manifestWithNoTools);

      const loadedTools = await client.loadToolset(toolsetName);

      expect(loadedTools).toEqual([]);
      expect(MockedCreateZodSchemaFromParams).not.toHaveBeenCalled();
      expect(MockedToolboxToolFactory).not.toHaveBeenCalled();
    });

    it('should throw an error if manifest parsing fails for toolset', async () => {
      const toolsetName = 'bad-manifest-set';
      const expectedApiUrlForToolset = `${testBaseUrl}/api/toolset/${toolsetName}`;
      const mockApiResponseData = {invalid: 'toolset structure'};
      const mockZodError = new ZodError([
        {
          input: undefined,
          path: ['serverVersion'],
          message: 'Zod validation failed on toolset',
          code: 'custom',
        },
      ]);

      mockSessionGet.mockResolvedValueOnce({
        data: mockApiResponseData,
      } as AxiosResponse);
      MockedZodManifestSchema.parse.mockImplementationOnce(() => {
        throw mockZodError;
      });

      await expect(client.loadToolset(toolsetName)).rejects.toThrow(
        `Invalid manifest structure received from ${expectedApiUrlForToolset}: ${JSON.stringify(mockZodError.issues, null, 2)}`,
      );
    });

    it('should throw and log error if API GET request for toolset fails', async () => {
      const toolsetName = 'api-error-set';
      const expectedApiUrl = `${testBaseUrl}/api/toolset/${toolsetName}`;
      const apiError = new Error('Toolset API unavailable');
      mockSessionGet.mockRejectedValueOnce(apiError);

      await expect(client.loadToolset(toolsetName)).rejects.toThrow(apiError);
      expect(consoleErrorSpy).toHaveBeenCalledWith(
        `Error fetching data from ${expectedApiUrl}:`,
        apiError.message,
      );
    });

    describe('in strict mode', () => {
      it('should throw an error for unused bound parameters on a per-tool basis', async () => {
        const toolsetName = 'strict-set';
        const mockToolDefinitions: Record<string, InferredZodTool> = {
          toolA: {
            description: 'Tool A',
            parameters: [{name: 'paramA', type: 'string'} as ParameterSchema],
          },
        };
        const boundParams = {paramA: 'valA', unusedParam: 'valUnused'};
        setupMocksForSuccessfulToolsetLoad(mockToolDefinitions);

        await expect(
          client.loadToolset(toolsetName, {}, boundParams, true),
        ).rejects.toThrow(
          "Validation failed for tool 'toolA': unused bound parameters: unusedParam.",
        );
      });

      it('should throw an error for unused auth tokens on a per-tool basis', async () => {
        const toolsetName = 'strict-set-auth';
        const mockToolDefinitions: Record<string, InferredZodTool> = {
          toolA: {
            description: 'Tool A',
            parameters: [
              {
                name: 'tokenA',
                type: 'string',
                authSources: ['ServiceA'],
              } as ParameterSchema,
            ],
          },
        };
        const authTokenGetters = {
          ServiceA: () => 'token-a',
          ServiceB: () => 'token-b',
        };
        setupMocksForSuccessfulToolsetLoad(mockToolDefinitions);

        await expect(
          client.loadToolset(toolsetName, authTokenGetters, {}, true),
        ).rejects.toThrow(
          "Validation failed for tool 'toolA': unused auth tokens: ServiceB.",
        );
      });
    });
  });
});
