import { describe, it, expect, beforeEach, afterEach } 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, BrowserSessionRepo } from '../db/repository.js';
import { createBrowserSessionApi } from './browser-session-api.js';
import { initMasterKey, generateUserDek, encryptUserDek } from '../crypto/sessions.js';
import type { SessionManager } from '../engine/browser-session.js';

interface TestContext {
  app: express.Application;
  repository: Repository;
  sessRepo: BrowserSessionRepo;
  tempDir: string;
}

function buildApp(userId: string): TestContext {
  const tempDir = mkdtempSync(join(tmpdir(), 'maestro-bsapi-'));
  const dbPath = join(tempDir, 'orchestrator.db');
  const repository = new Repository(dbPath);
  const db = repository.getDb();
  db.prepare(`INSERT INTO users (id, email, role, status, created_at, updated_at)
              VALUES (?, ?, 'active', 'active', datetime('now'), datetime('now'))`)
    .run(userId, `${userId}@test`);
  const sessRepo = new BrowserSessionRepo(db);
  const masterKeyPath = join(tempDir, 'master.key');
  const master = initMasterKey(masterKeyPath);
  // Pre-seed a DEK for the user
  sessRepo.setUserDek(userId, encryptUserDek(master, generateUserDek()));

  const app = express();
  app.use(express.json());
  // Stub req.user for auth-required tests
  app.use((req, _res, next) => {
    (req as { user?: unknown }).user = { id: userId, role: 'active' };
    next();
  });
  app.use('/api/browser-sessions', createBrowserSessionApi({ sessRepo, sessionManager: null, masterKeyPath }));
  return { app, repository, sessRepo, tempDir };
}

describe('browser-session-api', () => {
  let ctx: TestContext | null = null;

  afterEach(() => {
    if (ctx) {
      ctx.repository.close();
      rmSync(ctx.tempDir, { recursive: true, force: true });
      ctx = null;
    }
  });

  it('lists empty for a new user', async () => {
    ctx = buildApp('u1');
    const res = await request(ctx.app).get('/api/browser-sessions/profiles');
    expect(res.status).toBe(200);
    expect(res.body.profiles).toEqual([]);
  });

  it('creates a profile (status=pending, label echoes input)', async () => {
    ctx = buildApp('u1');
    const res = await request(ctx.app).post('/api/browser-sessions/profiles').send({
      label: 'GitHub',
      startUrl: 'https://github.com',
      matchPatterns: ['https://github.com/**'],
      storageOrigins: ['https://github.com'],
      loginUrlPatterns: ['https://github.com/login**'],
    });
    expect(res.status).toBe(201);
    expect(res.body.profile.status).toBe('pending');
    expect(res.body.profile.label).toBe('GitHub');
    // Must NOT leak the encrypted blob
    expect(res.body.profile.encryptedStateBlob).toBeUndefined();
    expect(res.body.profile.encrypted_state_blob).toBeUndefined();
  });

  it('deletes only profiles the user owns', async () => {
    ctx = buildApp('u1');
    // Owned profile: 200
    const id = ctx.sessRepo.createProfile({
      ownerId: 'u1', label: 'X', startUrl: 'https://x.com',
      matchPatterns: [], storageOrigins: [], loginUrlPatterns: [],
    });
    const ok = await request(ctx.app).delete(`/api/browser-sessions/profiles/${id}`);
    expect(ok.status).toBe(200);

    // Non-existent: 404
    const missing = await request(ctx.app).delete(`/api/browser-sessions/profiles/9999`);
    expect(missing.status).toBe(404);

    // Other user's profile: 404 (owner enforcement)
    ctx.repository.getDb().prepare(`INSERT INTO users (id, email, role, status, created_at, updated_at)
                                    VALUES ('u2','u2@test','active','active',datetime('now'),datetime('now'))`).run();
    const otherId = ctx.sessRepo.createProfile({
      ownerId: 'u2', label: 'Other', startUrl: 'https://other.com',
      matchPatterns: [], storageOrigins: [], loginUrlPatterns: [],
    });
    const forbidden = await request(ctx.app).delete(`/api/browser-sessions/profiles/${otherId}`);
    expect(forbidden.status).toBe(404);
    // Confirm it was NOT actually deleted (still exists for u2)
    expect(ctx.sessRepo.getProfileById(otherId, 'u2')).not.toBeNull();
  });

  it('rejects unauthenticated requests', async () => {
    const tempDir = mkdtempSync(join(tmpdir(), 'maestro-bsapi-noauth-'));
    const dbPath = join(tempDir, 'orchestrator.db');
    const repository = new Repository(dbPath);
    const sessRepo = new BrowserSessionRepo(repository.getDb());
    const masterKeyPath = join(tempDir, 'master.key');

    const app = express();
    app.use(express.json());
    // No req.user middleware → unauthenticated
    app.use('/api/browser-sessions', createBrowserSessionApi({ sessRepo, sessionManager: null, masterKeyPath }));

    const res = await request(app).get('/api/browser-sessions/profiles');
    expect(res.status).toBe(401);
    expect(res.body.error).toBe('Unauthenticated');

    repository.close();
    rmSync(tempDir, { recursive: true, force: true });
  });

  // ── P2b: authActive-aware gate ─────────────────────────────────────────────

  it('authActive=true still rejects unauthenticated requests', async () => {
    const tempDir = mkdtempSync(join(tmpdir(), 'maestro-bsapi-auth-'));
    const dbPath = join(tempDir, 'orchestrator.db');
    const repository = new Repository(dbPath);
    const sessRepo = new BrowserSessionRepo(repository.getDb());
    const masterKeyPath = join(tempDir, 'master.key');

    const app = express();
    app.use(express.json());
    // authActive=true + no req.user → must still return 401
    app.use('/api/browser-sessions', createBrowserSessionApi({
      sessRepo,
      sessionManager: null,
      masterKeyPath,
      authActive: true,
    }));

    const res = await request(app).get('/api/browser-sessions/profiles');
    expect(res.status).toBe(401);
    expect(res.body.error).toBe('Unauthenticated');

    repository.close();
    rmSync(tempDir, { recursive: true, force: true });
  });

  it('authActive=false falls back to synthetic local user (no-auth mode)', async () => {
    const tempDir = mkdtempSync(join(tmpdir(), 'maestro-bsapi-noauth-local-'));
    const dbPath = join(tempDir, 'orchestrator.db');
    const repository = new Repository(dbPath);
    const db = repository.getDb();
    // Insert local user so FK constraints pass
    db.prepare(`INSERT INTO users (id, email, role, status, created_at, updated_at)
                VALUES ('local', 'local@localhost', 'active', 'active', datetime('now'), datetime('now'))`).run();
    const sessRepo = new BrowserSessionRepo(db);
    const masterKeyPath = join(tempDir, 'master.key');

    const app = express();
    app.use(express.json());
    // No req.user middleware, but authActive=false → should inject synthetic local user
    app.use('/api/browser-sessions', createBrowserSessionApi({
      sessRepo,
      sessionManager: null,
      masterKeyPath,
      authActive: false,
    }));

    const res = await request(app).get('/api/browser-sessions/profiles');
    expect(res.status).toBe(200);
    expect(res.body.profiles).toEqual([]);

    repository.close();
    rmSync(tempDir, { recursive: true, force: true });
  });
});

describe('login + save flow', () => {
  let tempDirsToClean: string[] = [];
  let repositoryToClose: Repository | null = null;

  afterEach(() => {
    if (repositoryToClose) {
      repositoryToClose.close();
      repositoryToClose = null;
    }
    for (const d of tempDirsToClean) {
      rmSync(d, { recursive: true, force: true });
    }
    tempDirsToClean = [];
  });

  it('starts a login session, then save captures storageState and encrypts it', async () => {
    const tempDir = mkdtempSync(join(tmpdir(), 'maestro-bsapi-loginflow-'));
    tempDirsToClean.push(tempDir);
    const dbPath = join(tempDir, 'orchestrator.db');
    const repository = new Repository(dbPath);
    repositoryToClose = repository;
    const db = repository.getDb();
    db.prepare(`INSERT INTO users (id, email, role, status, created_at, updated_at)
                VALUES (?, ?, 'active', 'active', datetime('now'), datetime('now'))`)
      .run('u1', 'u1@test');
    const sessRepo = new BrowserSessionRepo(db);
    const masterKeyPath = join(tempDir, 'master.key');
    // Intentionally NOT pre-seeding a DEK — /save should lazily create one via ensureUserDek.

    const id = sessRepo.createProfile({
      ownerId: 'u1',
      label: 'X',
      startUrl: 'https://example.com',
      matchPatterns: [],
      storageOrigins: [],
      loginUrlPatterns: [],
    });

    const fakeContext = {
      pages: () => [],
      newPage: async () => ({ goto: async () => null }),
      storageState: async () => ({ cookies: [{ name: 's', value: '1' }], origins: [] }),
    };
    const fake = {
      createLoginSession: async (_opts: unknown) => ({
        id: 'sess1',
        kind: 'login',
        profileId: id,
        context: fakeContext,
        browser: { isConnected: () => true },
        display: ':99',
      }),
      getSession: () => ({
        id: 'sess1',
        kind: 'login',
        profileId: id,
        context: fakeContext,
      }),
      destroySession: async () => {},
    };

    const app = express();
    app.use(express.json());
    app.use((req, _res, next) => {
      (req as { user?: unknown }).user = { id: 'u1', role: 'active' };
      next();
    });
    app.use('/api/browser-sessions', createBrowserSessionApi({
      sessRepo,
      sessionManager: fake as unknown as SessionManager,
      masterKeyPath,
    }));

    const start = await request(app).post(`/api/browser-sessions/profiles/${id}/login`);
    expect(start.status).toBe(200);
    expect(start.body.sessionId).toBe('sess1');
    expect(start.body.novncPath).toContain('sess1');

    const save = await request(app)
      .post(`/api/browser-sessions/profiles/${id}/save`)
      .send({ sessionId: 'sess1' });
    expect(save.status).toBe(200);

    const profile = sessRepo.getProfileById(id, 'u1')!;
    expect(profile).not.toBeNull();
    expect(profile.status).toBe('active');
    expect(profile.encryptedStateBlob).not.toBeNull();
    expect(profile.encryptedStateBlob!.length).toBeGreaterThan(16);

    // Sanity: the API response itself must NOT leak the encrypted blob.
    expect(save.body.profile.encryptedStateBlob).toBeUndefined();
    expect(save.body.profile.encrypted_state_blob).toBeUndefined();
    expect(save.body.profile.status).toBe('active');
  });
});
