import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
import express from 'express';
import request from 'supertest';
import { Repository } from '../db/repository.js';
import { BrowserSessionRepo } from '../db/browser-session-repo.js';
import { Scheduler } from '../scheduler.js';
import { mountScheduledTasksApi } from './scheduled-tasks-api.js';
import { mkdtempSync, rmSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';

let app: express.Application;
let repo: Repository;
let scheduler: Scheduler;
let tempDir: string;

beforeAll(() => {
  tempDir = mkdtempSync(join(tmpdir(), 'agent-sched-api-'));
  repo = new Repository(join(tempDir, 'test.db'));
  scheduler = new Scheduler(repo, join(tempDir, 'workspaces'));
  app = express();
  app.use(express.json());
  mountScheduledTasksApi(app, repo, scheduler);
});

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

describe('POST /api/scheduled-tasks with visibility', () => {
  let vTempDir = '';
  let vRepo: Repository;
  let vApp: express.Application;
  let aliceUser: Express.User;

  beforeEach(() => {
    vTempDir = mkdtempSync(join(tmpdir(), 'sched-vis-api-'));
    vRepo = new Repository(join(vTempDir, 'db.sqlite'));
    const real = vRepo.createUser({ email: 'a@x.com', name: 'a', role: 'user', status: 'active' });
    aliceUser = {
      ...real,
      orgIds: ['10'],
      defaultVisibility: 'private',
      defaultVisibilityOrgId: null,
    };
    const vScheduler = new Scheduler(vRepo, join(vTempDir, 'workspaces'));
    vApp = express();
    vApp.use(express.json());
    vApp.use((req, _res, next) => {
      (req as unknown as { user: Express.User }).user = aliceUser;
      next();
    });
    mountScheduledTasksApi(vApp, vRepo, vScheduler);
  });

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

  it('creates scheduled task with owner_id set from req.user and visibility=org', async () => {
    const res = await request(vApp).post('/api/scheduled-tasks').send({
      body: 'hello',
      scheduleType: 'daily',
      hour: 9,
      minute: 0,
      visibility: 'org',
      visibilityScopeOrgId: '10',
    });
    expect(res.status).toBe(201);
    expect(res.body.task.visibility).toBe('org');
    expect(res.body.task.visibilityScopeOrgId).toBe('10');
    expect(res.body.task.ownerId).toBe(aliceUser.id);
  });

  it('defaults visibility to private and owner from req.user when not provided', async () => {
    const res = await request(vApp).post('/api/scheduled-tasks').send({
      body: 'hello',
      scheduleType: 'daily',
      hour: 10,
    });
    expect(res.status).toBe(201);
    expect(res.body.task.visibility).toBe('private');
    expect(res.body.task.visibilityScopeOrgId).toBeNull();
    expect(res.body.task.ownerId).toBe(aliceUser.id);
  });

  it('rejects visibility=org with org not in user orgs', async () => {
    const res = await request(vApp).post('/api/scheduled-tasks').send({
      body: 'hello',
      scheduleType: 'daily',
      hour: 9,
      visibility: 'org',
      visibilityScopeOrgId: '99',
    });
    expect(res.status).toBe(400);
  });

  it('rejects invalid visibility enum values', async () => {
    const res = await request(vApp).post('/api/scheduled-tasks').send({
      body: 'hello',
      scheduleType: 'daily',
      hour: 9,
      visibility: 'bogus',
    });
    expect(res.status).toBe(400);
  });
});

describe('POST /api/scheduled-tasks in no-auth mode (synthetic local owner)', () => {
  let nTempDir = '';
  let nRepo: Repository;
  let nApp: express.Application;

  beforeEach(() => {
    nTempDir = mkdtempSync(join(tmpdir(), 'sched-noauth-api-'));
    nRepo = new Repository(join(nTempDir, 'db.sqlite'));
    const nScheduler = new Scheduler(nRepo, join(nTempDir, 'workspaces'));
    nApp = express();
    nApp.use(express.json());
    // No user middleware: no-auth deployment (req.user undefined). authActive:false
    // tells the router to register the scheduled task under the 'local' owner so
    // the jobs the scheduler later spawns inherit it (not NULL).
    mountScheduledTasksApi(nApp, nRepo, nScheduler, { authActive: false });
  });

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

  it('registers the scheduled task under owner "local" instead of NULL', async () => {
    const res = await request(nApp).post('/api/scheduled-tasks').send({
      body: 'nightly thing',
      scheduleType: 'daily',
      hour: 9,
      minute: 0,
    });
    expect(res.status).toBe(201);
    expect(res.body.task.ownerId).toBe('local');
  });
});

describe('POST /api/scheduled-tasks', () => {
  it('should create a daily schedule', async () => {
    const res = await request(app)
      .post('/api/scheduled-tasks')
      .send({
        title: 'テスト日次',
        body: 'テストプロンプト',
        scheduleType: 'daily',
        hour: 9,
        minute: 0,
      });
    expect(res.status).toBe(201);
    expect(res.body.task.cronExpression).toBe('0 9 * * *');
    expect(res.body.task.isActive).toBe(true);
  });

  it('should require body', async () => {
    const res = await request(app).post('/api/scheduled-tasks').send({ scheduleType: 'daily' });
    expect(res.status).toBe(400);
  });
});

describe('GET /api/scheduled-tasks', () => {
  it('should list all scheduled tasks', async () => {
    const res = await request(app).get('/api/scheduled-tasks');
    expect(res.status).toBe(200);
    expect(Array.isArray(res.body.tasks)).toBe(true);
  });
});

describe('PATCH /api/scheduled-tasks/:id', () => {
  it('should pause and resume', async () => {
    const createRes = await request(app)
      .post('/api/scheduled-tasks')
      .send({ title: 'pause-test', body: 'test', scheduleType: 'daily', hour: 10 });
    const id = createRes.body.task.id;

    const pauseRes = await request(app).patch(`/api/scheduled-tasks/${id}`).send({ isActive: false });
    expect(pauseRes.body.task.isActive).toBe(false);

    const resumeRes = await request(app).patch(`/api/scheduled-tasks/${id}`).send({ isActive: true });
    expect(resumeRes.body.task.isActive).toBe(true);
  });
});

describe('DELETE /api/scheduled-tasks/:id', () => {
  it('should delete a scheduled task', async () => {
    const createRes = await request(app)
      .post('/api/scheduled-tasks')
      .send({ title: 'delete-test', body: 'test', scheduleType: 'daily', hour: 10 });
    const id = createRes.body.task.id;

    const delRes = await request(app).delete(`/api/scheduled-tasks/${id}`);
    expect(delRes.status).toBe(200);

    const getRes = await request(app).get(`/api/scheduled-tasks/${id}`);
    expect(getRes.status).toBe(404);
  });
});

describe('PATCH/DELETE /api/scheduled-tasks/:id owner-or-admin', () => {
  let pTempDir = '';
  let pRepo: Repository;

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

  function buildAppForUser(user: Express.User): express.Application {
    const pScheduler = new Scheduler(pRepo, join(pTempDir, 'workspaces'));
    const pApp = express();
    pApp.use(express.json());
    pApp.use((req, _res, next) => {
      (req as unknown as { user: Express.User }).user = user;
      next();
    });
    mountScheduledTasksApi(pApp, pRepo, pScheduler);
    return pApp;
  }

  function seedTask(ownerId: string, visibility: 'private' | 'org' | 'public' = 'public') {
    return pRepo.createScheduledTask({
      title: 't',
      body: 'b',
      cronExpression: '0 9 * * *',
      nextRunAt: '2099-01-01 09:00:00',
      ownerId,
      visibility,
    });
  }

  it('non-owner non-admin gets 404 on PATCH (even when visibility=public)', async () => {
    pTempDir = mkdtempSync(join(tmpdir(), 'sched-perm-'));
    pRepo = new Repository(join(pTempDir, 'db.sqlite'));

    const alice = pRepo.createUser({ email: 'a@x.com', name: 'a', role: 'user', status: 'active' });
    const task = await seedTask(alice.id, 'public');

    const bobUser: Express.User = {
      id: 'bob-id',
      email: 'b@x.com',
      name: 'b',
      avatarUrl: null,
      role: 'user',
      status: 'active',
      orgIds: [],
      defaultVisibility: 'private',
      defaultVisibilityOrgId: null,
    };
    const pApp = buildAppForUser(bobUser);

    const res = await request(pApp)
      .patch(`/api/scheduled-tasks/${task.id}`)
      .send({ title: 'edited' });
    expect(res.status).toBe(404);

    // Task title not changed
    const after = await pRepo.getScheduledTask(task.id);
    expect(after?.title).toBe('t');
  });

  it('non-owner non-admin gets 404 on DELETE', async () => {
    pTempDir = mkdtempSync(join(tmpdir(), 'sched-perm-'));
    pRepo = new Repository(join(pTempDir, 'db.sqlite'));

    const alice = pRepo.createUser({ email: 'a@x.com', name: 'a', role: 'user', status: 'active' });
    const task = await seedTask(alice.id, 'public');

    const bobUser: Express.User = {
      id: 'bob-id',
      email: 'b@x.com',
      name: 'b',
      avatarUrl: null,
      role: 'user',
      status: 'active',
      orgIds: [],
      defaultVisibility: 'private',
      defaultVisibilityOrgId: null,
    };
    const pApp = buildAppForUser(bobUser);

    const res = await request(pApp).delete(`/api/scheduled-tasks/${task.id}`);
    expect(res.status).toBe(404);

    const after = await pRepo.getScheduledTask(task.id);
    expect(after).not.toBeNull();
  });

  it('admin can PATCH any scheduled task', async () => {
    pTempDir = mkdtempSync(join(tmpdir(), 'sched-perm-'));
    pRepo = new Repository(join(pTempDir, 'db.sqlite'));

    const alice = pRepo.createUser({ email: 'a@x.com', name: 'a', role: 'user', status: 'active' });
    const task = await seedTask(alice.id, 'private');

    const adminUser: Express.User = {
      id: 'admin-id',
      email: 'admin@x.com',
      name: 'admin',
      avatarUrl: null,
      role: 'admin',
      status: 'active',
      orgIds: [],
      defaultVisibility: 'private',
      defaultVisibilityOrgId: null,
    };
    const pApp = buildAppForUser(adminUser);

    const res = await request(pApp)
      .patch(`/api/scheduled-tasks/${task.id}`)
      .send({ title: 'edited-by-admin' });
    expect(res.status).toBe(200);
    expect(res.body.task.title).toBe('edited-by-admin');
  });

  it('admin can DELETE any scheduled task', async () => {
    pTempDir = mkdtempSync(join(tmpdir(), 'sched-perm-'));
    pRepo = new Repository(join(pTempDir, 'db.sqlite'));

    const alice = pRepo.createUser({ email: 'a@x.com', name: 'a', role: 'user', status: 'active' });
    const task = await seedTask(alice.id, 'private');

    const adminUser: Express.User = {
      id: 'admin-id',
      email: 'admin@x.com',
      name: 'admin',
      avatarUrl: null,
      role: 'admin',
      status: 'active',
      orgIds: [],
      defaultVisibility: 'private',
      defaultVisibilityOrgId: null,
    };
    const pApp = buildAppForUser(adminUser);

    const res = await request(pApp).delete(`/api/scheduled-tasks/${task.id}`);
    expect(res.status).toBe(200);
    expect(res.body.ok).toBe(true);

    const after = await pRepo.getScheduledTask(task.id);
    expect(after).toBeNull();
  });

  it('owner can PATCH own scheduled task', async () => {
    pTempDir = mkdtempSync(join(tmpdir(), 'sched-perm-'));
    pRepo = new Repository(join(pTempDir, 'db.sqlite'));

    const alice = pRepo.createUser({ email: 'a@x.com', name: 'a', role: 'user', status: 'active' });
    const aliceUser: Express.User = {
      ...alice,
      orgIds: [],
      defaultVisibility: 'private',
      defaultVisibilityOrgId: null,
    };
    const task = await seedTask(alice.id, 'private');

    const pApp = buildAppForUser(aliceUser);
    const res = await request(pApp)
      .patch(`/api/scheduled-tasks/${task.id}`)
      .send({ title: 'edited-by-owner' });
    expect(res.status).toBe(200);
    expect(res.body.task.title).toBe('edited-by-owner');
  });

  it('owner can DELETE own scheduled task', async () => {
    pTempDir = mkdtempSync(join(tmpdir(), 'sched-perm-'));
    pRepo = new Repository(join(pTempDir, 'db.sqlite'));

    const alice = pRepo.createUser({ email: 'a@x.com', name: 'a', role: 'user', status: 'active' });
    const aliceUser: Express.User = {
      ...alice,
      orgIds: [],
      defaultVisibility: 'private',
      defaultVisibilityOrgId: null,
    };
    const task = await seedTask(alice.id, 'private');

    const pApp = buildAppForUser(aliceUser);
    const res = await request(pApp).delete(`/api/scheduled-tasks/${task.id}`);
    expect(res.status).toBe(200);
    expect(res.body.ok).toBe(true);
  });
});

describe('GET /api/scheduled-tasks visibility filter', () => {
  let lTempDir = '';
  let lRepo: Repository;

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

  function buildAppForUser(user: Express.User): express.Application {
    const lScheduler = new Scheduler(lRepo, join(lTempDir, 'workspaces'));
    const lApp = express();
    lApp.use(express.json());
    lApp.use((req, _res, next) => {
      (req as unknown as { user: Express.User }).user = user;
      next();
    });
    mountScheduledTasksApi(lApp, lRepo, lScheduler);
    return lApp;
  }

  it('non-owner does not see private scheduled tasks in list', async () => {
    lTempDir = mkdtempSync(join(tmpdir(), 'sched-list-'));
    lRepo = new Repository(join(lTempDir, 'db.sqlite'));
    const alice = lRepo.createUser({ email: 'a@x.com', name: 'a', role: 'user', status: 'active' });
    await lRepo.createScheduledTask({
      title: 'alice-private', body: 'b',
      cronExpression: '0 9 * * *', nextRunAt: '2099-01-01 09:00:00',
      ownerId: alice.id, visibility: 'private',
    });

    const bobUser: Express.User = {
      id: 'bob-id', email: 'b@x.com', name: 'b', avatarUrl: null,
      role: 'user', status: 'active', orgIds: [],
      defaultVisibility: 'private', defaultVisibilityOrgId: null,
    };
    const res = await request(buildAppForUser(bobUser)).get('/api/scheduled-tasks');
    expect(res.status).toBe(200);
    expect(res.body.tasks.map((t: { title: string }) => t.title)).not.toContain('alice-private');
  });

  it('owner sees own private scheduled tasks', async () => {
    lTempDir = mkdtempSync(join(tmpdir(), 'sched-list-'));
    lRepo = new Repository(join(lTempDir, 'db.sqlite'));
    const alice = lRepo.createUser({ email: 'a@x.com', name: 'a', role: 'user', status: 'active' });
    const aliceUser: Express.User = {
      ...alice, orgIds: [],
      defaultVisibility: 'private', defaultVisibilityOrgId: null,
    };
    await lRepo.createScheduledTask({
      title: 'alice-private', body: 'b',
      cronExpression: '0 9 * * *', nextRunAt: '2099-01-01 09:00:00',
      ownerId: alice.id, visibility: 'private',
    });

    const res = await request(buildAppForUser(aliceUser)).get('/api/scheduled-tasks');
    expect(res.status).toBe(200);
    expect(res.body.tasks.map((t: { title: string }) => t.title)).toContain('alice-private');
  });

  it('admin sees all scheduled tasks regardless of visibility', async () => {
    lTempDir = mkdtempSync(join(tmpdir(), 'sched-list-'));
    lRepo = new Repository(join(lTempDir, 'db.sqlite'));
    const alice = lRepo.createUser({ email: 'a@x.com', name: 'a', role: 'user', status: 'active' });
    await lRepo.createScheduledTask({
      title: 'alice-private', body: 'b',
      cronExpression: '0 9 * * *', nextRunAt: '2099-01-01 09:00:00',
      ownerId: alice.id, visibility: 'private',
    });

    const adminUser: Express.User = {
      id: 'admin-id', email: 'admin@x.com', name: 'admin', avatarUrl: null,
      role: 'admin', status: 'active', orgIds: [],
      defaultVisibility: 'private', defaultVisibilityOrgId: null,
    };
    const res = await request(buildAppForUser(adminUser)).get('/api/scheduled-tasks');
    expect(res.status).toBe(200);
    expect(res.body.tasks.map((t: { title: string }) => t.title)).toContain('alice-private');
  });
});

describe('POST /api/scheduled-tasks browserSessionProfileId owner check', () => {
  let bTempDir = '';
  let bRepo: Repository;
  let bSessRepo: BrowserSessionRepo;
  let alice: { id: string };
  let bob: { id: string };
  let aliceProfileId: number;
  let bobProfileId: number;

  beforeEach(() => {
    bTempDir = mkdtempSync(join(tmpdir(), 'sched-bsp-'));
    bRepo = new Repository(join(bTempDir, 'db.sqlite'));
    bSessRepo = new BrowserSessionRepo(bRepo.getDb());
    alice = bRepo.createUser({ email: 'a@x.com', name: 'a', role: 'user', status: 'active' });
    bob = bRepo.createUser({ email: 'b@x.com', name: 'b', role: 'user', status: 'active' });
    aliceProfileId = bSessRepo.createProfile({
      ownerId: alice.id,
      label: 'alice-twitter',
      startUrl: 'https://twitter.com/home',
      matchPatterns: ['https://twitter.com/**'],
      storageOrigins: ['https://twitter.com'],
      loggedInSelector: null,
      loginUrlPatterns: [],
    });
    bobProfileId = bSessRepo.createProfile({
      ownerId: bob.id,
      label: 'bob-twitter',
      startUrl: 'https://twitter.com/home',
      matchPatterns: ['https://twitter.com/**'],
      storageOrigins: ['https://twitter.com'],
      loggedInSelector: null,
      loginUrlPatterns: [],
    });
  });

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

  function buildAppForUser(user: Express.User): express.Application {
    const bScheduler = new Scheduler(bRepo, join(bTempDir, 'workspaces'));
    const bApp = express();
    bApp.use(express.json());
    bApp.use((req, _res, next) => {
      (req as unknown as { user: Express.User }).user = user;
      next();
    });
    mountScheduledTasksApi(bApp, bRepo, bScheduler, { sessRepo: bSessRepo });
    return bApp;
  }

  function asUser(u: { id: string }, email: string): Express.User {
    return {
      id: u.id, email, name: 'x', avatarUrl: null,
      role: 'user', status: 'active', orgIds: [],
      defaultVisibility: 'private', defaultVisibilityOrgId: null,
    };
  }

  it('accepts a valid profile owned by the requesting user (201)', async () => {
    const res = await request(buildAppForUser(asUser(alice, 'a@x.com')))
      .post('/api/scheduled-tasks')
      .send({
        body: 'hello',
        scheduleType: 'daily',
        hour: 9,
        browserSessionProfileId: aliceProfileId,
      });
    expect(res.status).toBe(201);
    expect(res.body.task.browserSessionProfileId).toBe(aliceProfileId);
  });

  it('rejects a profile owned by a different user (400)', async () => {
    const res = await request(buildAppForUser(asUser(alice, 'a@x.com')))
      .post('/api/scheduled-tasks')
      .send({
        body: 'hello',
        scheduleType: 'daily',
        hour: 9,
        browserSessionProfileId: bobProfileId,
      });
    expect(res.status).toBe(400);
    expect(res.body.error).toMatch(/not owned by you|not found/i);
  });

  it('rejects a positive integer that does not match any profile (400)', async () => {
    const res = await request(buildAppForUser(asUser(alice, 'a@x.com')))
      .post('/api/scheduled-tasks')
      .send({
        body: 'hello',
        scheduleType: 'daily',
        hour: 9,
        browserSessionProfileId: 999999,
      });
    expect(res.status).toBe(400);
  });
});
