import { beforeEach, describe, expect, it, vi } from 'vitest';
import { Hono } from 'hono';
import { DAEMON_COMMAND_TYPES } from '../../shared/daemon-command-types.js';
import {
  WORKER_SESSION_SNAPSHOT_INCOMPLETE_REASON,
  WORKER_SESSION_SNAPSHOT_ROUTE_SEGMENT,
} from '../../shared/worker-session-snapshot.js';

const mockResolveServerRole = vi.fn<() => Promise<string>>().mockResolvedValue('owner');
const mockUpsertDbSession = vi.fn();
const mockUpdateSession = vi.fn();
const mockGetDbSessionsByServer = vi.fn<(...args: unknown[]) => Promise<unknown[]>>(async () => []);
const mockGetSubSessionsByServer = vi.fn<(...args: unknown[]) => Promise<unknown[]>>(async () => []);
const sendToDaemonMock = vi.fn();

vi.mock('../src/security/authorization.js', () => ({
  requireAuth: () => async (c: { set: (key: string, value: string) => void }, next: () => Promise<void>) => {
    c.set('userId', 'user-1');
    c.set('role', 'owner');
    await next();
  },
  resolveServerRole: (...args: unknown[]) => mockResolveServerRole(...args as []),
}));

vi.mock('../src/db/queries.js', () => ({
  getServerById: vi.fn(async () => ({ id: 'srv-1' })),
  getDbSessionsByServer: (...args: unknown[]) => mockGetDbSessionsByServer(...args),
  getSubSessionsByServer: (...args: unknown[]) => mockGetSubSessionsByServer(...args),
  upsertDbSession: (...args: unknown[]) => mockUpsertDbSession(...args),
  deleteDbSession: vi.fn(),
  updateSessionLabel: vi.fn(),
  updateProjectName: vi.fn(),
  updateSession: (...args: unknown[]) => mockUpdateSession(...args),
}));

vi.mock('../src/security/crypto.js', () => ({
  randomHex: vi.fn(() => 'sid-test'),
}));

vi.mock('../src/ws/bridge.js', () => ({
  WsBridge: {
    get: () => ({
      sendToDaemon: sendToDaemonMock,
    }),
  },
}));

vi.mock('../src/util/pod-identity.js', () => ({
  getPodIdentity: vi.fn(() => 'pod-a'),
}));

describe('session-mgmt persistence routes', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    mockResolveServerRole.mockResolvedValue('owner');
    mockGetDbSessionsByServer.mockResolvedValue([]);
    mockGetSubSessionsByServer.mockResolvedValue([]);
  });

  async function buildApp() {
    const { sessionMgmtRoutes } = await import('../src/routes/session-mgmt.js');
    const app = new Hono();
    app.use('*', async (c, next) => {
      (c as unknown as { env: { DB: object } }).env = { DB: {} };
      await next();
    });
    app.route('/api/server', sessionMgmtRoutes);
    return app;
  }

  it('GET /session-snapshot returns paired complete metadata for daemon sync', async () => {
    mockGetDbSessionsByServer.mockResolvedValueOnce([
      {
        name: 'deck_proj_brain',
        project_name: 'proj',
        role: 'brain',
        agent_type: 'claude-code',
        project_dir: '/tmp/proj',
        state: 'idle',
        label: null,
        requested_model: null,
        active_model: null,
        effort: null,
        transport_config: {},
      },
    ]);
    mockGetSubSessionsByServer.mockResolvedValueOnce([
      {
        id: 'child',
        type: 'claude-code',
        cwd: '/tmp/proj/sub',
        parent_session: 'deck_proj_brain',
        label: null,
        requested_model: null,
        active_model: null,
        effort: null,
        transport_config: {},
      },
    ]);
    const app = await buildApp();

    const res = await app.request(`/api/server/srv-1/${WORKER_SESSION_SNAPSHOT_ROUTE_SEGMENT}`);
    const body = await res.json() as {
      complete: boolean;
      serverId: string;
      counts: { sessions: number; subSessions: number };
      sessions: Array<{ name: string }>;
      subSessions: Array<{ id: string }>;
    };

    expect(res.status).toBe(200);
    expect(body.complete).toBe(true);
    expect(body.serverId).toBe('srv-1');
    expect(body.counts).toEqual({ sessions: 1, subSessions: 1 });
    expect(body.sessions).toEqual([expect.objectContaining({ name: 'deck_proj_brain' })]);
    expect(body.subSessions).toEqual([expect.objectContaining({ id: 'child' })]);
  });

  it('GET /session-snapshot returns incomplete instead of complete empty when the query fails', async () => {
    mockGetDbSessionsByServer.mockRejectedValueOnce(new Error('db down'));
    const app = await buildApp();

    const res = await app.request(`/api/server/srv-1/${WORKER_SESSION_SNAPSHOT_ROUTE_SEGMENT}`);
    const body = await res.json() as { complete: boolean; reason: string };

    expect(res.status).toBe(503);
    expect(body).toMatchObject({
      complete: false,
      reason: WORKER_SESSION_SNAPSHOT_INCOMPLETE_REASON.QUERY_FAILED,
    });
  });

  it('PUT /sessions/:name persists label plus requestedModel/activeModel/effort/transportConfig', async () => {
    const app = await buildApp();
    const res = await app.request('/api/server/srv-1/sessions/deck_proj_brain', {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        projectName: 'proj',
        projectRole: 'brain',
        agentType: 'claude-code-sdk',
        projectDir: '/tmp/proj',
        state: 'idle',
        label: 'Readable Main',
        runtimeType: 'transport',
        providerId: 'claude-code-sdk',
        providerSessionId: 'route-1',
        description: 'persona',
        requestedModel: 'sonnet',
        activeModel: 'sonnet',
        effort: 'high',
        transportConfig: { provider: { mode: 'safe' } },
      }),
    });

    expect(res.status).toBe(200);
    expect(mockUpsertDbSession).toHaveBeenCalledWith(
      {},
      'sid-test',
      'srv-1',
      'deck_proj_brain',
      'proj',
      'brain',
      'claude-code-sdk',
      '/tmp/proj',
      'idle',
      'Readable Main',
      null,
      'transport',
      'claude-code-sdk',
      'route-1',
      'persona',
      'sonnet',
      'sonnet',
      'high',
      { provider: { mode: 'safe' } },
    );
  });

  it('PUT /sessions/:name ignores known test sessions', async () => {
    const app = await buildApp();
    const res = await app.request('/api/server/srv-1/sessions/deck_bootmainabc123_brain', {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        projectName: 'bootmainabc123',
        projectRole: 'brain',
        agentType: 'claude-code-sdk',
        projectDir: '/tmp/bootmain-e2e',
        state: 'idle',
      }),
    });

    expect(res.status).toBe(200);
    expect(await res.json()).toEqual({ ok: true, ignored: 'test_session' });
    expect(mockUpsertDbSession).not.toHaveBeenCalled();
  });

  it('POST /session/start rejects known test sessions before relaying to daemon', async () => {
    const app = await buildApp();
    const res = await app.request('/api/server/srv-1/session/start', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        project: 'bootmainabc123',
        dir: '/tmp/bootmain-e2e',
        agentType: 'claude-code-sdk',
      }),
    });

    expect(res.status).toBe(400);
    expect(await res.json()).toEqual({ error: 'test_session_blocked' });
    expect(sendToDaemonMock).not.toHaveBeenCalled();
  });

  it('PATCH /sessions/:name updates requestedModel/activeModel/effort/transportConfig', async () => {
    const app = await buildApp();
    const res = await app.request('/api/server/srv-1/sessions/deck_proj_brain', {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        requestedModel: 'gpt-5.4',
        activeModel: 'gpt-5.4',
        effort: 'medium',
        transportConfig: { provider: { mode: 'balanced' } },
      }),
    });

    expect(res.status).toBe(200);
    expect(mockUpdateSession).toHaveBeenCalledWith(
      {},
      'srv-1',
      'deck_proj_brain',
      {
        requested_model: 'gpt-5.4',
        active_model: 'gpt-5.4',
        effort: 'medium',
        transport_config: { provider: { mode: 'balanced' } },
      },
    );
  });

  it('PATCH /sessions/:name relays session.restart when agentType changes', async () => {
    const app = await buildApp();
    const res = await app.request('/api/server/srv-1/sessions/deck_proj_brain', {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        agentType: 'codex-sdk',
        cwd: '/tmp/next',
        description: 'next persona',
      }),
    });

    expect(res.status).toBe(200);
    expect(mockUpdateSession).toHaveBeenCalledWith(
      {},
      'srv-1',
      'deck_proj_brain',
      {
        description: 'next persona',
        project_dir: '/tmp/next',
      },
    );
    expect(sendToDaemonMock).toHaveBeenCalledTimes(1);
    expect(JSON.parse(String(sendToDaemonMock.mock.calls[0]?.[0]))).toEqual({
      type: 'session.restart',
      sessionName: 'deck_proj_brain',
      agentType: 'codex-sdk',
      cwd: '/tmp/next',
      description: 'next persona',
    });
  });

  it('PATCH /sessions/:name relays transport-config updates to the daemon without a restart', async () => {
    const app = await buildApp();
    const res = await app.request('/api/server/srv-1/sessions/deck_proj_brain', {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        transportConfig: { supervision: { mode: 'supervised' } },
      }),
    });

    expect(res.status).toBe(200);
    expect(mockUpdateSession).toHaveBeenCalledWith(
      {},
      'srv-1',
      'deck_proj_brain',
      {
        transport_config: { supervision: { mode: 'supervised' } },
      },
    );
    expect(JSON.parse(String(sendToDaemonMock.mock.calls[0]?.[0]))).toEqual({
      type: DAEMON_COMMAND_TYPES.SESSION_UPDATE_TRANSPORT_CONFIG,
      sessionName: 'deck_proj_brain',
      transportConfig: { supervision: { mode: 'supervised' } },
    });
  });

  it('POST /session/cancel relays direct SDK cancel without /stop text', async () => {
    const app = await buildApp();
    const res = await app.request('/api/server/srv-1/session/cancel', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ sessionName: 'deck_proj_brain', commandId: 'cancel-1', text: '/stop' }),
    });

    expect(res.status).toBe(200);
    expect(JSON.parse(String(sendToDaemonMock.mock.calls[0]?.[0]))).toEqual({
      type: DAEMON_COMMAND_TYPES.SESSION_CANCEL,
      sessionName: 'deck_proj_brain',
      commandId: 'cancel-1',
    });
  });

  it('PATCH /sessions/:name/rename updates the project name and relays session.rename', async () => {
    const { updateProjectName } = await import('../src/db/queries.js');
    const app = await buildApp();
    const res = await app.request('/api/server/srv-1/sessions/deck_proj_brain/rename', {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ name: 'new-proj' }),
    });

    expect(res.status).toBe(200);
    expect(updateProjectName).toHaveBeenCalledWith({}, 'srv-1', 'deck_proj_brain', 'new-proj');
    expect(sendToDaemonMock).toHaveBeenCalledTimes(1);
    expect(JSON.parse(String(sendToDaemonMock.mock.calls[0]?.[0]))).toEqual({
      type: 'session.rename',
      sessionName: 'deck_proj_brain',
      projectName: 'new-proj',
    });
  });

  it('PATCH /sessions/:name/label updates the label and relays session.relabel', async () => {
    const { updateSessionLabel } = await import('../src/db/queries.js');
    const app = await buildApp();
    const res = await app.request('/api/server/srv-1/sessions/deck_proj_brain/label', {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ label: 'Main Label' }),
    });

    expect(res.status).toBe(200);
    expect(updateSessionLabel).toHaveBeenCalledWith({}, 'srv-1', 'deck_proj_brain', 'Main Label');
    expect(JSON.parse(String(sendToDaemonMock.mock.calls[0]?.[0]))).toEqual({
      type: 'session.relabel',
      sessionName: 'deck_proj_brain',
      label: 'Main Label',
    });
  });

  it('PATCH /sessions/:name/label allows clearing the label and still relays session.relabel', async () => {
    const { updateSessionLabel } = await import('../src/db/queries.js');
    const app = await buildApp();
    const res = await app.request('/api/server/srv-1/sessions/deck_proj_brain/label', {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ label: '' }),
    });

    expect(res.status).toBe(200);
    expect(updateSessionLabel).toHaveBeenCalledWith({}, 'srv-1', 'deck_proj_brain', null);
    expect(JSON.parse(String(sendToDaemonMock.mock.calls[0]?.[0]))).toEqual({
      type: 'session.relabel',
      sessionName: 'deck_proj_brain',
      label: null,
    });
  });
});
