import { describe, expect, it, vi } from 'vitest';
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { tmpdir } from 'node:os';

import {
  configureComposioConfigStore,
  deleteComposioAuthConfigId,
  readComposioConfig,
  readPublicComposioConfig,
  setComposioAuthConfigId,
  writeComposioConfig,
} from '../src/connectors/composio-config.js';
import { composioConnectorProvider, getStaticComposioCatalogDefinitions } from '../src/connectors/composio.js';
import type { ConnectorCatalogDefinition } from '../src/connectors/catalog.js';

async function useTempComposioStore(): Promise<string> {
  const dir = await mkdtemp(path.join(tmpdir(), 'od-composio-config-'));
  configureComposioConfigStore(dir);
  composioConnectorProvider.clearDiscoveryCache();
  return dir;
}

function composioJson(body: unknown, status = 200): Response {
  return new Response(JSON.stringify(body), {
    status,
    headers: { 'content-type': 'application/json' },
  });
}

function composioDefinition(id = 'github'): ConnectorCatalogDefinition {
  return {
    id,
    name: id,
    provider: 'composio',
    category: 'code',
    authentication: 'composio',
    tools: [],
    allowedToolNames: [],
  };
}

describe('composio config', () => {
  it('stores Composio settings in the configured data directory', async () => {
    const dir = await useTempComposioStore();

    const publicConfig = writeComposioConfig({
      apiKey: 'cmp_secret_1234',
    });

    expect(publicConfig).toEqual({
      configured: true,
      apiKeyTail: '1234',
    });
    expect(readComposioConfig()).toMatchObject({ apiKey: 'cmp_secret_1234', authConfigIds: {} });
    await expect(readFile(path.join(dir, 'connectors', 'composio-config.json'), 'utf8')).resolves.toContain('cmp_secret_1234');
  });

  it('preserves and updates persisted auth config ids', async () => {
    await useTempComposioStore();
    writeComposioConfig({ apiKey: 'stored_secret', authConfigIds: { github: 'ac_github' } });

    writeComposioConfig({});
    setComposioAuthConfigId('slack', 'ac_slack');
    deleteComposioAuthConfigId('github');

    expect(readComposioConfig()).toEqual({
      apiKey: 'stored_secret',
      authConfigIds: { slack: 'ac_slack' },
    });
  });

  it('does not read Composio credentials from environment variables', async () => {
    await useTempComposioStore();
    const originalApiKey = process.env.COMPOSIO_API_KEY;
    try {
      process.env.COMPOSIO_API_KEY = 'env_secret';

      expect(readPublicComposioConfig()).toMatchObject({ configured: false, apiKeyTail: '' });
      expect(composioConnectorProvider.isConfigured(composioDefinition())).toBe(false);

      writeComposioConfig({ apiKey: 'stored_secret' });
      expect(readPublicComposioConfig()).toMatchObject({ configured: true, apiKeyTail: 'cret' });
    } finally {
      if (originalApiKey === undefined) delete process.env.COMPOSIO_API_KEY;
      else process.env.COMPOSIO_API_KEY = originalApiKey;
    }
  });

  it('can clear the stored API key through settings', async () => {
    await useTempComposioStore();
    writeComposioConfig({ apiKey: 'stored_secret' });

    const publicConfig = writeComposioConfig({ apiKey: '' });

    expect(publicConfig.configured).toBe(false);
    expect(composioConnectorProvider.isConfigured(composioDefinition())).toBe(false);
    expect(readComposioConfig()).toEqual({ apiKey: '', authConfigIds: {} });
  });

  it('clears stored auth config ids when the API key changes', async () => {
    await useTempComposioStore();
    writeComposioConfig({ apiKey: 'stored_secret', authConfigIds: { github: 'ac_github' } });

    const publicConfig = writeComposioConfig({ apiKey: 'new_secret' });

    expect(publicConfig).toEqual({ configured: true, apiKeyTail: 'cret' });
    expect(readComposioConfig()).toEqual({ apiKey: 'new_secret', authConfigIds: {} });
  });

  it('ignores stale unsupported persisted technical fields', async () => {
    await useTempComposioStore();
    writeComposioConfig({ apiKey: 'stored_secret' });

    const publicConfig = writeComposioConfig({ apiKey: '', baseUrl: '', userId: '', timeoutMs: null });

    expect(publicConfig).toEqual({ configured: false, apiKeyTail: '' });
    expect(readComposioConfig()).toEqual({ apiKey: '', authConfigIds: {} });
  });

  it('loads persisted Composio catalog cache into fast definitions', async () => {
    const dir = await useTempComposioStore();
    await mkdir(path.join(dir, 'connectors'), { recursive: true });
    await writeFile(path.join(dir, 'connectors', 'composio-catalog-cache.json'), JSON.stringify({
      schemaVersion: 1,
      provider: 'composio',
      fetchedAt: '2026-05-07T00:00:00.000Z',
      definitions: [
        {
          id: 'slack',
          name: 'Slack',
          provider: 'composio',
          category: 'Communication',
          providerConnectorId: 'SLACK',
          authentication: 'composio',
          toolCount: 48,
          tools: [
            {
              name: 'slack.slack_list_channels',
              title: 'List channels',
              description: 'List Slack channels',
              safety: { sideEffect: 'read', approval: 'auto', reason: 'read-only' },
              refreshEligible: true,
              curation: { useCases: ['personal_daily_digest'], reason: 'Digest source' },
              requiredScopes: ['read'],
              providerToolId: 'SLACK_LIST_CHANNELS',
            },
          ],
          allowedToolNames: ['slack.slack_list_channels'],
          minimumApproval: 'auto',
        },
      ],
    }, null, 2));

    composioConnectorProvider.configureCatalogCache(dir);

    expect(composioConnectorProvider.getFastDefinitions().find((definition) => definition.id === 'slack')).toMatchObject({
      id: 'slack',
      toolCount: 48,
      tools: [expect.objectContaining({
        name: 'slack.slack_list_channels',
        curation: expect.objectContaining({ useCases: ['personal_daily_digest'] }),
      })],
    });
  });

  it('falls back to the static catalog when the persisted cache is empty', async () => {
    const dir = await useTempComposioStore();
    await mkdir(path.join(dir, 'connectors'), { recursive: true });
    await writeFile(path.join(dir, 'connectors', 'composio-catalog-cache.json'), JSON.stringify({
      schemaVersion: 1,
      provider: 'composio',
      fetchedAt: '2026-05-07T00:00:00.000Z',
      definitions: [],
    }, null, 2));

    composioConnectorProvider.configureCatalogCache(dir);

    expect(composioConnectorProvider.getFastDefinitions()).toEqual(getStaticComposioCatalogDefinitions());
  });

  it('does not hydrate persisted catalog cache before the runtime data directory is configured', async () => {
    const defaultCacheDir = path.join(process.cwd(), '.od', 'connectors');
    const defaultCachePath = path.join(defaultCacheDir, 'composio-catalog-cache.json');
    const dir = await useTempComposioStore();
    await mkdir(defaultCacheDir, { recursive: true });
    await writeFile(defaultCachePath, JSON.stringify({
      schemaVersion: 1,
      provider: 'composio',
      fetchedAt: '2026-05-07T00:00:00.000Z',
      definitions: [composioDefinition('wrong-tenant')],
    }, null, 2));

    try {
      vi.resetModules();
      const composioModule = await import('../src/connectors/composio.js');

      expect(composioModule.composioConnectorProvider.getFastDefinitions().find((definition) => definition.id === 'wrong-tenant')).toBeUndefined();

      await mkdir(path.join(dir, 'connectors'), { recursive: true });
      await writeFile(path.join(dir, 'connectors', 'composio-catalog-cache.json'), JSON.stringify({
        schemaVersion: 1,
        provider: 'composio',
        fetchedAt: '2026-05-07T00:00:00.000Z',
        definitions: [composioDefinition('right-tenant')],
      }, null, 2));

      composioModule.composioConnectorProvider.configureCatalogCache(dir);

      expect(composioModule.composioConnectorProvider.getFastDefinitions().find((definition) => definition.id === 'right-tenant')).toMatchObject({
        id: 'right-tenant',
      });
    } finally {
      await rm(defaultCachePath, { force: true });
    }
  });

  it('treats the current Notion search action as read-only despite broad response-size wording', async () => {
    await useTempComposioStore();
    writeComposioConfig({ apiKey: 'cmp_test' });
    const originalFetch = globalThis.fetch;
    globalThis.fetch = vi.fn(async (input: Parameters<typeof fetch>[0]) => {
      const parsed = new URL(input.toString(), 'https://backend.composio.dev');
      if (parsed.pathname === '/api/v3/auth_configs') {
        return composioJson({
          items: [
            { id: 'ac_notion', status: 'ENABLED', toolkit: { slug: 'notion' } },
          ],
        });
      }
      if (parsed.pathname === '/api/v3.1/toolkits') {
        return composioJson({
          items: [
            { slug: 'notion', name: 'Notion', categories: [{ name: 'Productivity' }] },
          ],
        });
      }
      if (
        parsed.pathname === '/api/v3.1/tools'
        && parsed.searchParams.get('toolkit_slug') === 'notion'
      ) {
        return composioJson({
          items: [
            {
              slug: 'NOTION_SEARCH_NOTION_PAGE',
              name: 'Search Notion pages and databases',
              description:
                'Searches Notion pages and databases by title. Database pages can create large responses for databases with many properties.',
              toolkit: { slug: 'notion' },
              input_parameters: {
                type: 'object',
                properties: {
                  query: { type: 'string' },
                  page_size: { type: 'integer', minimum: 1, maximum: 100 },
                },
                additionalProperties: false,
              },
              tags: [],
            },
          ],
          total_items: 1,
        });
      }
      return composioJson({ items: [] });
    }) as typeof fetch;

    try {
      const definition = await composioConnectorProvider.getPreviewDefinition('notion', {
        toolsLimit: 1000,
      });
      const tool = definition?.tools.find(
        (candidate) => candidate.name === 'notion.notion_search_notion_page',
      );

      expect(definition?.allowedToolNames).toContain('notion.notion_search_notion_page');
      expect(tool).toMatchObject({
        refreshEligible: true,
        safety: { sideEffect: 'read', approval: 'auto' },
        curation: expect.objectContaining({ useCases: ['personal_daily_digest'] }),
      });
    } finally {
      globalThis.fetch = originalFetch;
    }
  });
});
