import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import Database from 'better-sqlite3';
import { runMigrations } from '../db/migrate.js';
import { createConnectionRepo, type SshConnection } from './connection-repo.js';
import { createGrantsRepo } from './grants-repo.js';
import { createAccessResolver } from './access.js';

const validKey = 'a'.repeat(64);

function bootstrapDb(): Database.Database {
  process.env.MCP_ENCRYPTION_KEY = validKey;
  const db = new Database(':memory:');
  db.pragma('foreign_keys = ON');
  db.exec(`CREATE TABLE users (id TEXT PRIMARY KEY);`);
  db.exec(`CREATE TABLE jobs (id TEXT PRIMARY KEY, wait_reason TEXT);`);
  db.exec(`CREATE TABLE local_tasks (id INTEGER PRIMARY KEY AUTOINCREMENT);`);
  runMigrations(db);
  db.prepare(`INSERT INTO users(id) VALUES (?), (?), (?)`).run('alice', 'bob', 'admin');
  return db;
}

function mkConnection(
  db: Database.Database,
  ownerId: string | null,
  spaceId: string | null = null,
): SshConnection {
  const repo = createConnectionRepo(db);
  return repo.create({
    ownerId,
    spaceId,
    label: 'srv',
    host: 'h',
    port: 22,
    username: 'u',
    privateKeyEnc: Buffer.from([1]),
    keyFingerprint: 'SHA256:x',
    remotePathPrefix: '/home/u',
  });
}

describe('ssh/access resolveAccess', () => {
  let db: Database.Database;
  beforeEach(() => {
    db = bootstrapDb();
  });
  afterEach(() => {
    db.close();
    delete process.env.MCP_ENCRYPTION_KEY;
  });

  it('owner: allow via=owner', () => {
    const conn = mkConnection(db, 'alice');
    const grants = createGrantsRepo(db);
    const resolver = createAccessResolver(grants, { adminBypassesGrants: true });
    const r = resolver.resolveAccess({
      connection: conn,
      userId: 'alice',
      isAdmin: false,
      pieceName: 'devops',
      orgIds: [],
    });
    expect(r.allowed).toBe(true);
    expect(r.via).toBe('owner');
  });

  it('admin with bypass enabled: allow via=admin', () => {
    const conn = mkConnection(db, 'alice');
    const grants = createGrantsRepo(db);
    const resolver = createAccessResolver(grants, { adminBypassesGrants: true });
    const r = resolver.resolveAccess({
      connection: conn,
      userId: 'admin',
      isAdmin: true,
      pieceName: 'devops',
      orgIds: [],
    });
    expect(r.allowed).toBe(true);
    expect(r.via).toBe('admin');
  });

  it('admin without bypass: requires a grant', () => {
    const conn = mkConnection(db, 'alice');
    const grants = createGrantsRepo(db);
    const resolver = createAccessResolver(grants, { adminBypassesGrants: false });
    const r = resolver.resolveAccess({
      connection: conn,
      userId: 'admin',
      isAdmin: true,
      pieceName: 'devops',
      orgIds: [],
    });
    expect(r.allowed).toBe(false);
    expect(r.reason).toBe('no_grant');
  });

  it('non-owner non-admin with active grant: allow via=grant', () => {
    const conn = mkConnection(db, 'alice');
    const grants = createGrantsRepo(db);
    grants.create({
      connectionId: conn.id,
      subjectType: 'user',
      subjectId: 'bob',
      pieceName: 'devops',
      appliesToAllPieces: false,
      grantedByUserId: 'admin',
      reason: 'on-call escalation',
    });
    const resolver = createAccessResolver(grants, { adminBypassesGrants: true });
    const r = resolver.resolveAccess({
      connection: conn,
      userId: 'bob',
      isAdmin: false,
      pieceName: 'devops',
      orgIds: [],
    });
    expect(r.allowed).toBe(true);
    expect(r.via).toBe('grant');
    expect(r.grant?.subjectId).toBe('bob');
  });

  it('non-owner non-admin without a grant: deny no_grant', () => {
    const conn = mkConnection(db, 'alice');
    const grants = createGrantsRepo(db);
    const resolver = createAccessResolver(grants, { adminBypassesGrants: true });
    const r = resolver.resolveAccess({
      connection: conn,
      userId: 'bob',
      isAdmin: false,
      pieceName: 'devops',
      orgIds: [],
    });
    expect(r.allowed).toBe(false);
    expect(r.reason).toBe('no_grant');
  });

  it('disabled connection: deny even for owner', () => {
    const conn = mkConnection(db, 'alice');
    const repo = createConnectionRepo(db);
    repo.update(conn.id, { enabled: false });
    const refreshed = repo.resolveConnection(conn.id)!;
    const grants = createGrantsRepo(db);
    const resolver = createAccessResolver(grants, { adminBypassesGrants: true });
    const r = resolver.resolveAccess({
      connection: refreshed,
      userId: 'alice',
      isAdmin: false,
      pieceName: 'devops',
      orgIds: [],
    });
    expect(r.allowed).toBe(false);
    expect(r.reason).toBe('disabled');
  });

  it('disabled connection: deny even for admin', () => {
    const conn = mkConnection(db, 'alice');
    const repo = createConnectionRepo(db);
    repo.disableByAdmin(conn.id, 'security incident', 'admin');
    const refreshed = repo.resolveConnection(conn.id)!;
    const grants = createGrantsRepo(db);
    const resolver = createAccessResolver(grants, { adminBypassesGrants: true });
    const r = resolver.resolveAccess({
      connection: refreshed,
      userId: 'admin',
      isAdmin: true,
      pieceName: 'devops',
      orgIds: [],
    });
    expect(r.allowed).toBe(false);
    expect(r.reason).toBe('disabled');
  });

  it('global connection (ownerId=null): non-owner, non-admin with grant works', () => {
    const conn = mkConnection(db, null);
    const grants = createGrantsRepo(db);
    grants.create({
      connectionId: conn.id,
      subjectType: 'org',
      subjectId: 'org-acme',
      pieceName: 'devops',
      appliesToAllPieces: false,
      grantedByUserId: 'admin',
      reason: 'team needs prod access',
    });
    const resolver = createAccessResolver(grants, { adminBypassesGrants: false });
    const r = resolver.resolveAccess({
      connection: conn,
      userId: 'bob',
      isAdmin: false,
      pieceName: 'devops',
      orgIds: ['org-acme'],
    });
    expect(r.allowed).toBe(true);
    expect(r.via).toBe('grant');
  });

  // ── Per-space isolation (Phase 3, spec §11) ───────────────────────────────
  // Model = fully independent (space-only). Access to USE a space-scoped
  // connection = same-space membership. owner_id only picks the DEK downstream.

  it('space-scoped: user in space A is allowed a space-A connection (via=space)', () => {
    const conn = mkConnection(db, 'alice', 'space-A');
    const grants = createGrantsRepo(db);
    const resolver = createAccessResolver(grants, { adminBypassesGrants: true });
    const r = resolver.resolveAccess({
      connection: conn,
      userId: 'alice',
      isAdmin: false,
      pieceName: 'devops',
      orgIds: [],
      spaceId: 'space-A',
    });
    expect(r.allowed).toBe(true);
    expect(r.via).toBe('space');
  });

  it('space-scoped: cross-space DENIED even when the caller OWNS the connection', () => {
    // alice owns the connection, but her job runs in space A while the
    // connection lives in space B → denied (owner_id is no longer the key).
    const conn = mkConnection(db, 'alice', 'space-B');
    const grants = createGrantsRepo(db);
    const resolver = createAccessResolver(grants, { adminBypassesGrants: true });
    const r = resolver.resolveAccess({
      connection: conn,
      userId: 'alice',
      isAdmin: false,
      pieceName: 'devops',
      orgIds: [],
      spaceId: 'space-A',
    });
    expect(r.allowed).toBe(false);
    expect(r.reason).toBe('space_mismatch');
  });

  it('space-scoped: admin-with-bypass is DENIED a connection in another space', () => {
    const conn = mkConnection(db, 'alice', 'space-B');
    const grants = createGrantsRepo(db);
    const resolver = createAccessResolver(grants, { adminBypassesGrants: true });
    const r = resolver.resolveAccess({
      connection: conn,
      userId: 'admin',
      isAdmin: true,
      pieceName: 'devops',
      orgIds: [],
      spaceId: 'space-A',
    });
    expect(r.allowed).toBe(false);
    expect(r.reason).toBe('space_mismatch');
  });

  it('space-scoped connection is NOT visible to a no-space (legacy) job', () => {
    const conn = mkConnection(db, 'alice', 'space-A');
    const grants = createGrantsRepo(db);
    const resolver = createAccessResolver(grants, { adminBypassesGrants: true });
    const r = resolver.resolveAccess({
      connection: conn,
      userId: 'alice',
      isAdmin: false,
      pieceName: 'devops',
      orgIds: [],
      // no spaceId → legacy job; still cannot reach a space-scoped connection.
    });
    expect(r.allowed).toBe(false);
    expect(r.reason).toBe('space_mismatch');
  });

  it('legacy fallback: space-less connection + no-space job → owner allowed', () => {
    const conn = mkConnection(db, 'alice', null);
    const grants = createGrantsRepo(db);
    const resolver = createAccessResolver(grants, { adminBypassesGrants: true });
    const r = resolver.resolveAccess({
      connection: conn,
      userId: 'alice',
      isAdmin: false,
      pieceName: 'devops',
      orgIds: [],
      spaceId: null,
    });
    expect(r.allowed).toBe(true);
    expect(r.via).toBe('owner');
  });

  it('space-less connection is NOT visible to a space-scoped job', () => {
    // A legacy (space_id NULL) connection must not leak into a space job.
    const conn = mkConnection(db, 'alice', null);
    const grants = createGrantsRepo(db);
    const resolver = createAccessResolver(grants, { adminBypassesGrants: true });
    const r = resolver.resolveAccess({
      connection: conn,
      userId: 'alice',
      isAdmin: false,
      pieceName: 'devops',
      orgIds: [],
      spaceId: 'space-A',
    });
    expect(r.allowed).toBe(false);
    expect(r.reason).toBe('space_mismatch');
  });

  it('disabled space-scoped connection denies with reason=disabled (not space_mismatch)', () => {
    const conn = mkConnection(db, 'alice', 'space-A');
    const repo = createConnectionRepo(db);
    repo.update(conn.id, { enabled: false });
    const disabled = repo.resolveConnection(conn.id)!;
    const grants = createGrantsRepo(db);
    const resolver = createAccessResolver(grants, { adminBypassesGrants: true });
    const r = resolver.resolveAccess({
      connection: disabled,
      userId: 'alice',
      isAdmin: false,
      pieceName: 'devops',
      orgIds: [],
      spaceId: 'space-A',
    });
    expect(r.allowed).toBe(false);
    expect(r.reason).toBe('disabled');
  });
});
