import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import express from 'express';
import request from 'supertest';
import { mkdtempSync, rmSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { Repository } from '../db/repository.js';
import { mountLocalTasksApi } from './local-tasks-api.js';

// Functional tests for two previously-untested control routes:
//   PUT  /api/local/tasks/:taskId/mission         (APIC-007, gap: NONE)
//   POST /api/local/tasks/:taskId/regenerate-title (APIC-011, gap: PARTIAL)
// Uses a real temp-file Repository + the real mountLocalTasksApi so persistence
// is exercised end-to-end (mission brief round-trips through the DB).

let tempDir = '';
let repo: Repository;

function makeUser(overrides: Partial<Express.User> = {}): Express.User {
  return {
    id: 'owner-1',
    email: 'owner@x.com',
    name: 'Owner',
    avatarUrl: null,
    role: 'user',
    status: 'active',
    orgIds: [],
    defaultVisibility: 'private',
    defaultVisibilityOrgId: null,
    ...overrides,
  };
}

function makeApp(
  user: Express.User | undefined,
  opts: { generateTitle?: (body: string, ownerId?: string) => Promise<string> } = {},
): express.Application {
  const app = express();
  if (user) {
    app.use((req, _res, next) => {
      (req as unknown as { user: Express.User }).user = user;
      next();
    });
  }
  mountLocalTasksApi(app, {
    repo,
    worktreeDir: join(tempDir, 'workspaces'),
    generateTitle: opts.generateTitle,
  });
  return app;
}

// Create a task owned by owner-1 directly through the repo and return its id.
// ownerId is stored as a raw string column (no FK to users needed for these
// route checks, which compare task.ownerId === req.user.id), so we set it
// directly without seeding a users row.
async function seedTask(body = 'Investigate the flaky test in CI'): Promise<number> {
  const created = await repo.createLocalTask({
    title: 'seed task',
    body,
    pieceName: 'auto',
    ownerId: 'owner-1',
    visibility: 'private',
  });
  return created.id;
}

beforeEach(() => {
  tempDir = mkdtempSync(join(tmpdir(), 'maestro-mission-title-'));
  repo = new Repository(join(tempDir, 'db.sqlite'));
});

afterEach(() => {
  repo.close();
  rmSync(tempDir, { recursive: true, force: true });
});

describe('PUT /api/local/tasks/:taskId/mission', () => {
  it('persists string mission fields and reflects them on GET', async () => {
    const id = await seedTask();
    const app = makeApp(makeUser());

    const res = await request(app)
      .put(`/api/local/tasks/${id}/mission`)
      .send({ goal: 'Make CI green', done: 'all tests pass', open: 'which test', clarifications: '' });
    expect(res.status).toBe(200);
    expect(res.body.missionBrief).toMatchObject({
      goal: 'Make CI green', done: 'all tests pass', open: 'which test', clarifications: '',
    });

    // Persisted: a fresh GET reflects the saved brief.
    const got = await request(app).get(`/api/local/tasks/${id}`);
    expect(got.status).toBe(200);
    expect(got.body.task.missionBrief).toMatchObject({ goal: 'Make CI green', done: 'all tests pass' });
  });

  it('partial-replaces: only provided string fields change, others are kept', async () => {
    const id = await seedTask();
    const app = makeApp(makeUser());

    await request(app).put(`/api/local/tasks/${id}/mission`).send({ goal: 'first goal', open: 'q1' });
    const res = await request(app).put(`/api/local/tasks/${id}/mission`).send({ goal: 'second goal' });
    expect(res.status).toBe(200);
    expect(res.body.missionBrief.goal).toBe('second goal');
    // `open` was not sent → left unchanged.
    expect(res.body.missionBrief.open).toBe('q1');
  });

  it('rejects a body with no string mission fields (400)', async () => {
    const id = await seedTask();
    const app = makeApp(makeUser());
    // Non-string values are ignored → empty patch → 400.
    const res = await request(app).put(`/api/local/tasks/${id}/mission`).send({ goal: 123, foo: 'bar' });
    expect(res.status).toBe(400);
    expect(res.body.error).toContain('No mission fields');
  });

  it('denies a non-owner (404, no leak)', async () => {
    const id = await seedTask();
    const app = makeApp(makeUser({ id: 'intruder' }));
    const res = await request(app).put(`/api/local/tasks/${id}/mission`).send({ goal: 'hijack' });
    // checkTaskOwnership → 404 (private task invisible to non-owner; not 403).
    expect(res.status).toBe(404);
  });

  it('allows an admin to update any task mission', async () => {
    const id = await seedTask();
    const app = makeApp(makeUser({ id: 'admin-1', role: 'admin' }));
    const res = await request(app).put(`/api/local/tasks/${id}/mission`).send({ goal: 'admin set' });
    expect(res.status).toBe(200);
    expect(res.body.missionBrief.goal).toBe('admin set');
  });

  it('400 for a malformed taskId', async () => {
    const app = makeApp(makeUser());
    const res = await request(app).put('/api/local/tasks/not-a-number/mission').send({ goal: 'x' });
    expect(res.status).toBe(400);
  });
});

describe('POST /api/local/tasks/:taskId/regenerate-title', () => {
  it('regenerates the title for the owner via the generator', async () => {
    const id = await seedTask('Some long task body that needs a snappy title');
    const generateTitle = vi.fn().mockResolvedValue('  Snappy Title  ');
    const app = makeApp(makeUser(), { generateTitle });

    const res = await request(app).post(`/api/local/tasks/${id}/regenerate-title`);
    expect(res.status).toBe(200);
    expect(res.body.title).toBe('Snappy Title'); // trimmed
    expect(generateTitle).toHaveBeenCalledWith('Some long task body that needs a snappy title', 'owner-1');

    const got = await request(app).get(`/api/local/tasks/${id}`);
    expect(got.body.task.title).toBe('Snappy Title');
  });

  it('falls back to the synchronous title when the model returns empty', async () => {
    const id = await seedTask('Fallback body content');
    const generateTitle = vi.fn().mockResolvedValue('   ');
    const app = makeApp(makeUser(), { generateTitle });
    const res = await request(app).post(`/api/local/tasks/${id}/regenerate-title`);
    expect(res.status).toBe(200);
    // Empty model output is not an error — a non-empty fallback title is returned.
    expect(typeof res.body.title).toBe('string');
    expect(res.body.title.length).toBeGreaterThan(0);
  });

  it('denies a non-owner (404)', async () => {
    const id = await seedTask();
    const generateTitle = vi.fn().mockResolvedValue('Should not run');
    const app = makeApp(makeUser({ id: 'intruder' }), { generateTitle });
    const res = await request(app).post(`/api/local/tasks/${id}/regenerate-title`);
    expect(res.status).toBe(404);
    expect(generateTitle).not.toHaveBeenCalled();
  });

  it('allows an admin to regenerate any task title', async () => {
    const id = await seedTask();
    const generateTitle = vi.fn().mockResolvedValue('Admin Title');
    const app = makeApp(makeUser({ id: 'admin-1', role: 'admin' }), { generateTitle });
    const res = await request(app).post(`/api/local/tasks/${id}/regenerate-title`);
    expect(res.status).toBe(200);
    expect(res.body.title).toBe('Admin Title');
  });

  it('returns 503 when no title generator is configured', async () => {
    const id = await seedTask();
    const app = makeApp(makeUser()); // no generateTitle
    const res = await request(app).post(`/api/local/tasks/${id}/regenerate-title`);
    expect(res.status).toBe(503);
    expect(res.body.error).toContain('not configured');
  });

  it('returns 502 when the generator throws', async () => {
    const id = await seedTask();
    const generateTitle = vi.fn().mockRejectedValue(new Error('model down'));
    const app = makeApp(makeUser(), { generateTitle });
    const res = await request(app).post(`/api/local/tasks/${id}/regenerate-title`);
    expect(res.status).toBe(502);
  });
});
