/**
 * Integration tests for the goosed binary using the TypeScript API client.
 *
 * These tests spawn a real goosed process and issue requests via the
 * auto-generated API client to verify the server is working correctly.
 */

import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { setupGoosed, type GoosedTestContext } from './setup';
import {
  status,
  readConfig,
  providers,
  startAgent,
  stopAgent,
  listSessions,
  getSession,
  updateAgentProvider,
  updateSession,
  upsertConfig,
  reply,
} from '../../src/api';
import { execSync } from 'child_process';
import os from 'node:os';

const CONSTRAINED_PATH = '/usr/bin:/bin:/usr/sbin:/sbin';

function getUserPath(): string[] {
  try {
    const userShell = process.env.SHELL || '/bin/bash';
    const path = execSync(`${userShell} -l -i -c 'echo $PATH'`, {
      encoding: 'utf-8',
      timeout: 5000,
      env: {
        PATH: CONSTRAINED_PATH,
      },
    }).trim();

    const delimiter = process.platform === 'win32' ? ';' : ':';
    return path.split(delimiter).filter((entry: string) => entry.length > 0);
  } catch (error) {
    console.error('Error executing shell:', error);
    throw error;
  }
}

describe('goosed API integration tests', () => {
  let ctx: GoosedTestContext;

  beforeAll(async () => {
    const configYaml = `
extensions:
  developer:
    enabled: true
    type: builtin
    name: developer
    description: General development tools useful for software engineering.
    display_name: Developer
    timeout: 300
    bundled: true
    available_tools: []
`;

    ctx = await setupGoosed({ pathOverride: '/usr/bin:/bin', configYaml });
  });

  afterAll(async () => {
    await ctx.cleanup();
  });

  describe('health', () => {
    it('should respond to status endpoint', async () => {
      const response = await status({ client: ctx.client });
      expect(response.response).toBeOkResponse();
      expect(response.data).toBeDefined();
    });
  });

  describe('configuration', () => {
    it('should read config value (or return null for missing key)', async () => {
      const response = await readConfig({
        client: ctx.client,
        body: {
          key: 'GOOSE_PROVIDER',
          is_secret: false,
        },
      });
      expect(response.response).toBeOkResponse();
    });
  });

  describe('providers', () => {
    it('should list available providers', async () => {
      const response = await providers({ client: ctx.client });
      expect(response.response).toBeOkResponse();
      expect(response.data).toBeDefined();
      expect(Array.isArray(response.data)).toBe(true);
    });
  });

  describe('sessions', () => {
    it('should start an agent and create a session', async () => {
      const startResponse = await startAgent({
        client: ctx.client,
        body: {
          working_dir: os.tmpdir(),
        },
      });
      expect(startResponse.response).toBeOkResponse();
      expect(startResponse.data).toBeDefined();

      const session = startResponse.data!;
      expect(session.id).toBeDefined();
      expect(session.name).toBeDefined();
      expect(session.goose_mode).toBe('auto');

      const getResponse = await getSession({
        client: ctx.client,
        path: {
          session_id: session.id,
        },
      });
      expect(getResponse.response).toBeOkResponse();
      expect(getResponse.data).toBeDefined();
      expect(getResponse.data!.id).toBe(session.id);
    });

    it('should list sessions', async () => {
      const sessionsResponse = await listSessions({ client: ctx.client });
      expect(sessionsResponse.response).toBeOkResponse();
      expect(sessionsResponse.data).toBeDefined();
      expect(sessionsResponse.data!.sessions).toBeDefined();
      expect(Array.isArray(sessionsResponse.data!.sessions)).toBe(true);
    });

    it('should persist goose_mode on the session', async () => {
      await upsertConfig({
        client: ctx.client,
        body: { key: 'GOOSE_MODE', value: 'approve', is_secret: false },
      });

      try {
        const startResponse = await startAgent({
          client: ctx.client,
          body: { working_dir: os.tmpdir() },
        });
        expect(startResponse.response).toBeOkResponse();
        expect(startResponse.data!.goose_mode).toBe('approve');

        const getResponse = await getSession({
          client: ctx.client,
          path: { session_id: startResponse.data!.id },
        });
        expect(getResponse.response).toBeOkResponse();
        expect(getResponse.data!.goose_mode).toBe('approve');
      } finally {
        // Restore default so subsequent tests don't inherit approve mode
        await upsertConfig({
          client: ctx.client,
          body: { key: 'GOOSE_MODE', value: 'auto', is_secret: false },
        });
      }
    });

    it('should update goose_mode on an active session via /agent/update_session', async () => {
      const startResponse = await startAgent({
        client: ctx.client,
        body: { working_dir: os.tmpdir() },
      });
      expect(startResponse.response).toBeOkResponse();
      const sessionId = startResponse.data!.id;

      const updateResponse = await updateSession({
        client: ctx.client,
        body: { session_id: sessionId, goose_mode: 'approve' },
      });
      expect(updateResponse.response).toBeOkResponse();

      const getResponse = await getSession({
        client: ctx.client,
        path: { session_id: sessionId },
      });
      expect(getResponse.response).toBeOkResponse();
      expect(getResponse.data!.goose_mode).toBe('approve');
    });

    it('should preserve goose_mode after provider swap via /agent/update_provider', async (testContext) => {
      const configResponse = await readConfig({
        client: ctx.client,
        body: { key: 'GOOSE_PROVIDER', is_secret: false },
      });
      const providerName = configResponse.data as string | null | undefined;
      if (!providerName) {
        testContext.skip('Skipping - no GOOSE_PROVIDER configured');
        return;
      }

      const modelResponse = await readConfig({
        client: ctx.client,
        body: { key: 'GOOSE_MODEL', is_secret: false },
      });
      const modelName = (modelResponse.data as string | null) || undefined;

      const startResponse = await startAgent({
        client: ctx.client,
        body: { working_dir: os.tmpdir() },
      });
      expect(startResponse.response).toBeOkResponse();
      const sessionId = startResponse.data!.id;

      const updateResponse = await updateSession({
        client: ctx.client,
        body: { session_id: sessionId, goose_mode: 'approve' },
      });
      expect(updateResponse.response).toBeOkResponse();

      const providerResponse = await updateAgentProvider({
        client: ctx.client,
        body: {
          session_id: sessionId,
          provider: providerName,
          model: modelName,
        },
      });
      expect(providerResponse.response).toBeOkResponse();

      const getResponse = await getSession({
        client: ctx.client,
        path: { session_id: sessionId },
      });
      expect(getResponse.response).toBeOkResponse();
      expect(getResponse.data!.goose_mode).toBe('approve');
    });
  });

  describe('messaging', () => {
    it('should accept a message request to /reply endpoint', async () => {
      // Start a session first
      const startResponse = await startAgent({
        client: ctx.client,
        body: {
          working_dir: os.tmpdir(),
        },
      });
      expect(startResponse.response).toBeOkResponse();
      const sessionId = startResponse.data!.id;

      const abortController = new AbortController();
      const { stream } = await reply({
        client: ctx.client,
        body: {
          session_id: sessionId,
          user_message: {
            role: 'user',
            created: Math.floor(Date.now() / 1000),
            content: [
              {
                type: 'text',
                text: 'Hello',
              },
            ],
            metadata: {
              userVisible: true,
              agentVisible: true,
            },
          },
        },
        throwOnError: true,
        signal: abortController.signal,
      });

      const timeout = setTimeout(() => abortController.abort(), 1000);
      try {
        for await (const event of stream) {
          expect(event).toBeDefined();
          break;
        }
      } catch {
        // Aborted or error, that's fine
      }
      clearTimeout(timeout);

      await stopAgent({
        client: ctx.client,
        body: {
          session_id: sessionId,
        },
      });
    });
  });

  describe('the developer tool', () => {
    it('should see the full PATH when calling the developer tool', async (testContext) => {
      const currentPath = getUserPath();

      const pathEntry = currentPath.find((entry) => !CONSTRAINED_PATH.includes(entry));
      if (!pathEntry) {
        expect.fail(`Could not find a path entry not in ${CONSTRAINED_PATH}`);
      }

      let configResponse = await readConfig({
        client: ctx.client,
        body: {
          key: 'GOOSE_PROVIDER',
          is_secret: false,
        },
      });

      let providerName = configResponse.data as string | null | undefined;

      if (!providerName) {
        testContext.skip('Skipping tool execution test - no GOOSE_PROVIDER configured');
        return;
      }

      const modelResponse = await readConfig({
        client: ctx.client,
        body: {
          key: 'GOOSE_MODEL',
          is_secret: false,
        },
      });
      const modelName = (modelResponse.data as string | null) || undefined;

      const startResponse = await startAgent({
        client: ctx.client,
        body: {
          working_dir: os.tmpdir(),
        },
      });
      expect(startResponse.response).toBeOkResponse();
      const sessionId = startResponse.data!.id;

      const providerResponse = await updateAgentProvider({
        client: ctx.client,
        body: {
          session_id: sessionId,
          provider: providerName,
          model: modelName,
        },
      });
      expect(providerResponse.response).toBeOkResponse();

      const abortController = new AbortController();
      const { stream } = await reply({
        client: ctx.client,
        body: {
          session_id: sessionId,
          user_message: {
            role: 'user',
            created: Math.floor(Date.now() / 1000),
            content: [
              {
                type: 'text',
                text: 'Use your developer shell tool to read $PATH and return its content directly, with no further information about it',
              },
            ],
            metadata: {
              userVisible: true,
              agentVisible: true,
            },
          },
        },
        throwOnError: true,
        signal: abortController.signal,
      });

      let returnedPath: string | undefined = undefined;
      const timeout = setTimeout(() => abortController.abort(), 60000); // 60s timeout

      try {
        for await (const event of stream) {
          console.log('stream: ', JSON.stringify(event));

          if (event.type === 'Message') {
            const content = event.message?.content?.[0];
            if (content?.type === 'toolResponse') {
              const toolResult = content as {
                toolResult?: { value?: { content?: Array<{ text?: string }> } };
              };
              const output = toolResult?.toolResult?.value?.content?.[0]?.text;
              if (output && output.includes('/usr')) {
                clearTimeout(timeout);
                abortController.abort();
                returnedPath = output;
                break;
              }
            }
          }
        }
      } catch (error) {
        // Aborted or error
        if (!(error instanceof Error && error.name === 'AbortError')) {
          console.log('Stream error: ', error);
        }
      }
      clearTimeout(timeout);

      await stopAgent({
        client: ctx.client,
        body: {
          session_id: sessionId,
        },
      });

      expect(returnedPath, 'the agent should return a value for $PATH').toBeDefined();
      expect(returnedPath, '$PATH should contain the expected entry').toContain(pathEntry);
    });
  });
});
