import { getFunctionName } from 'convex/server';
import { afterEach, describe, expect, test, vi } from 'vitest';

import type { Id } from '@spoon/backend/convex/_generated/dataModel.js';

const mocks = vi.hoisted(() => ({
  getWorktreeDiff: vi.fn(() =>
    Promise.resolve({ exitCode: 0, output: 'unexpected diff' }),
  ),
  mutation: vi.fn(),
  streamExecInContainer: vi.fn(),
}));

vi.mock('convex/browser', () => ({
  ConvexHttpClient: vi.fn(
    class ConvexHttpClient {
      mutation = mocks.mutation;
    },
  ),
}));
vi.mock('../../src/env', () => ({
  env: {
    buildCreatedAt: 'test',
    buildSha: 'test',
    convexUrl: 'http://convex.test',
    jobTimeoutMs: 1000,
    runtime: 'docker',
    workerId: 'worker-1',
    workerToken: 'token',
  },
}));
vi.mock('../../src/git', () => ({
  cloneRepository: vi.fn(),
  commitAndPush: vi.fn(),
  getStatus: vi.fn(),
  getWorktreeDiff: mocks.getWorktreeDiff,
  run: vi.fn(),
}));
vi.mock('../../src/github', () => ({
  getInstallationToken: vi.fn(),
  openDraftPullRequest: vi.fn(),
}));
vi.mock('../../src/runtime/docker', () => ({
  buildMarkedCommand: vi.fn(() => ['setsid', 'bash', '-lc', '# marker']),
  killBoxProcessesByMarker: vi.fn(() => Promise.resolve()),
  listWorkspaceContainerNames: vi.fn(() => Promise.resolve([])),
  runExecInContainer: vi.fn(),
  streamExecInContainer: mocks.streamExecInContainer,
}));
vi.mock('../../src/user-container', () => ({
  acquireUserBox: vi.fn(),
  runningBoxUsernames: vi.fn(() => new Set()),
}));
vi.mock('../../src/user-environment', () => ({
  fetchUserEnvironment: vi.fn(),
  materializeUserHome: vi.fn(),
}));

const decision = {
  decision: 'sync',
  risk: 'low',
  summary: 'Safe to sync.',
  ignoredCommitShas: [],
  ignoredReason: '',
  recommendedAction: 'Sync upstream.',
  requiresUserApproval: false,
};

const load = async () => await import('../../src/worker');

afterEach(async () => {
  const worker = await load();
  worker._resetActiveWorkspacesForTests();
  vi.restoreAllMocks();
  vi.clearAllMocks();
});

describe('maintenance workspace cleanup', () => {
  test('releases and removes the workspace when status teardown fails', async () => {
    const worker = await load();
    const release = vi.fn();
    const jobId = 'maintenance-mark';
    mocks.mutation.mockImplementation((reference) => {
      if (getFunctionName(reference) === 'agentJobs:markWorkspaceStopped') {
        return Promise.reject(new Error('status teardown failed'));
      }
      return Promise.resolve('message-1');
    });
    mocks.streamExecInContainer.mockImplementation(async (args) => {
      await args.onStdoutLine(
        JSON.stringify({
          type: 'item.completed',
          item: {
            type: 'agent_message',
            text: JSON.stringify(decision),
          },
        }),
      );
      return { exitCode: 0, output: '' };
    });
    worker._setActiveWorkspaceForTests(jobId, {
      claim: {
        job: {
          _id: jobId as Id<'agentJobs'>,
          prompt: 'Review maintenance changes',
          jobType: 'maintenance_review',
          baseBranch: 'main',
          workBranch: 'spoon/maintenance',
          forkOwner: 'team',
          forkRepo: 'spoon',
          upstreamOwner: 'upstream',
          upstreamRepo: 'spoon',
        },
        spoon: { name: 'Spoon' },
        openai: {
          model: 'gpt-5',
          reasoningEffort: 'medium',
        },
        aiProviderProfile: {
          id: 'profile-1',
          name: 'Codex',
          provider: 'opencode_openai_login',
          authType: 'opencode_auth_json',
          model: 'gpt-5',
          reasoningEffort: 'medium',
        },
        github: {},
        secrets: [],
      },
      workdir: '/tmp/workspace',
      homeDir: '/tmp/workspace',
      username: 'tester',
      containerHome: '/home/tester',
      containerRepo: '/home/tester/Code/spoon',
      repoDir: '/tmp/workspace/Code/spoon',
      boxName: 'spoon-box-tester',
      boxHandle: { boxName: 'spoon-box-tester', release },
      githubToken: 'github-token',
      redact: (value: string) => value,
      runtime: 'codex',
    });
    const consoleError = vi
      .spyOn(console, 'error')
      .mockImplementation(() => undefined);

    await expect(
      worker.sendWorkspaceMessage(jobId, 'Review maintenance changes'),
    ).resolves.toBeUndefined();

    expect(release).toHaveBeenCalledTimes(1);
    expect(mocks.getWorktreeDiff).not.toHaveBeenCalled();
    expect(
      mocks.mutation.mock.calls.some(([, args]) => args.kind === 'diff'),
    ).toBe(false);
    await expect(worker.stopWorkspace(jobId)).rejects.toThrow(
      'Agent workspace is not active on this worker.',
    );
    expect(consoleError).toHaveBeenCalledTimes(1);
  });
});
