import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
import type { PrismaClient } from '@prisma/client';
import { setupTestDb, cleanupTestDb, clearAllTables, getTestClient } from './helpers.js';
import { seedTemplates } from '../src/seed/index.js';
import type { SeedTemplate } from '../src/seed/index.js';

// Wrap all tests in a single describe to scope lifecycle hooks
// and prevent leakage when running in the same worker as other test files.
describe('db models', () => {

let prisma: PrismaClient;

beforeAll(async () => {
  prisma = await setupTestDb();
}, 30_000);

afterAll(async () => {
  await cleanupTestDb();
});

beforeEach(async () => {
  await clearAllTables(prisma);
});

// ── Helper factories ──

async function createUser(overrides: { email?: string; name?: string; role?: 'USER' | 'ADMIN' } = {}) {
  return prisma.user.create({
    data: {
      email: overrides.email ?? `test-${Date.now()}@example.com`,
      name: overrides.name ?? 'Test User',
      passwordHash: '$2b$10$test-hash-placeholder',
      role: overrides.role ?? 'USER',
    },
  });
}

async function createGroup(overrides: { name?: string; description?: string } = {}) {
  return prisma.group.create({
    data: {
      name: overrides.name ?? `group-${Date.now()}`,
      description: overrides.description ?? 'Test group',
    },
  });
}

async function createProject(overrides: { name?: string; ownerId?: string } = {}) {
  let ownerId = overrides.ownerId;
  if (!ownerId) {
    const user = await createUser();
    ownerId = user.id;
  }
  return prisma.project.create({
    data: {
      name: overrides.name ?? `project-${Date.now()}`,
      ownerId,
    },
  });
}

async function createServer(overrides: { name?: string; transport?: 'STDIO' | 'SSE' | 'STREAMABLE_HTTP' } = {}) {
  return prisma.mcpServer.create({
    data: {
      name: overrides.name ?? `server-${Date.now()}`,
      description: 'Test server',
      packageName: '@test/mcp-server',
      transport: overrides.transport ?? 'STDIO',
    },
  });
}

// ── User model ──

describe('User', () => {
  it('creates a user with defaults', async () => {
    const user = await createUser();
    expect(user.id).toBeDefined();
    expect(user.role).toBe('USER');
    expect(user.version).toBe(1);
    expect(user.createdAt).toBeInstanceOf(Date);
    expect(user.updatedAt).toBeInstanceOf(Date);
  });

  it('enforces unique email', async () => {
    await createUser({ email: 'dup@test.com' });
    await expect(createUser({ email: 'dup@test.com' })).rejects.toThrow();
  });

  it('allows ADMIN role', async () => {
    const admin = await createUser({ role: 'ADMIN' });
    expect(admin.role).toBe('ADMIN');
  });

  it('updates updatedAt on change', async () => {
    const user = await createUser();
    const original = user.updatedAt;
    // Small delay to ensure different timestamp
    await new Promise((r) => setTimeout(r, 50));
    const updated = await prisma.user.update({
      where: { id: user.id },
      data: { name: 'Updated' },
    });
    expect(updated.updatedAt.getTime()).toBeGreaterThanOrEqual(original.getTime());
  });
});

// ── Session model ──

describe('Session', () => {
  it('creates a session linked to user', async () => {
    const user = await createUser();
    const session = await prisma.session.create({
      data: {
        token: 'test-token-123',
        userId: user.id,
        expiresAt: new Date(Date.now() + 86400_000),
      },
    });
    expect(session.token).toBe('test-token-123');
    expect(session.userId).toBe(user.id);
  });

  it('enforces unique token', async () => {
    const user = await createUser();
    const data = {
      token: 'unique-token',
      userId: user.id,
      expiresAt: new Date(Date.now() + 86400_000),
    };
    await prisma.session.create({ data });
    await expect(prisma.session.create({ data })).rejects.toThrow();
  });

  it('cascades delete when user is deleted', async () => {
    const user = await createUser();
    await prisma.session.create({
      data: {
        token: 'cascade-token',
        userId: user.id,
        expiresAt: new Date(Date.now() + 86400_000),
      },
    });
    await prisma.user.delete({ where: { id: user.id } });
    const sessions = await prisma.session.findMany({ where: { userId: user.id } });
    expect(sessions).toHaveLength(0);
  });
});

// ── McpServer model ──

describe('McpServer', () => {
  it('creates a server with defaults', async () => {
    const server = await createServer();
    expect(server.transport).toBe('STDIO');
    expect(server.version).toBe(1);
    expect(server.env).toEqual([]);
  });

  it('enforces unique name', async () => {
    await createServer({ name: 'slack' });
    await expect(createServer({ name: 'slack' })).rejects.toThrow();
  });

  it('stores env as JSON', async () => {
    const server = await prisma.mcpServer.create({
      data: {
        name: 'with-env',
        env: [
          { name: 'API_KEY', value: 'test-key' },
        ],
      },
    });
    const env = server.env as Array<{ name: string }>;
    expect(env).toHaveLength(1);
    expect(env[0].name).toBe('API_KEY');
  });

  it('supports SSE transport', async () => {
    const server = await createServer({ transport: 'SSE' });
    expect(server.transport).toBe('SSE');
  });
});

// ── Secret model ──

describe('Secret', () => {
  it('creates a secret with defaults', async () => {
    const secret = await prisma.secret.create({
      data: { name: 'my-secret' },
    });
    expect(secret.name).toBe('my-secret');
    expect(secret.data).toEqual({});
    expect(secret.version).toBe(1);
  });

  it('stores key-value data as JSON', async () => {
    const secret = await prisma.secret.create({
      data: {
        name: 'api-keys',
        data: { API_KEY: 'test-key', API_SECRET: 'test-secret' },
      },
    });
    const data = secret.data as Record<string, string>;
    expect(data['API_KEY']).toBe('test-key');
    expect(data['API_SECRET']).toBe('test-secret');
  });

  it('enforces unique name', async () => {
    await prisma.secret.create({ data: { name: 'dup-secret' } });
    await expect(prisma.secret.create({ data: { name: 'dup-secret' } })).rejects.toThrow();
  });

  it('updates data', async () => {
    const secret = await prisma.secret.create({
      data: { name: 'updatable', data: { KEY: 'old' } },
    });
    const updated = await prisma.secret.update({
      where: { id: secret.id },
      data: { data: { KEY: 'new', EXTRA: 'added' } },
    });
    const data = updated.data as Record<string, string>;
    expect(data['KEY']).toBe('new');
    expect(data['EXTRA']).toBe('added');
  });
});

// ── Project model ──

describe('Project', () => {
  it('creates a project with owner', async () => {
    const user = await createUser();
    const project = await prisma.project.create({
      data: { name: 'weekly-reports', ownerId: user.id },
    });
    expect(project.name).toBe('weekly-reports');
    expect(project.ownerId).toBe(user.id);
  });

  it('enforces unique project name', async () => {
    const user = await createUser();
    await prisma.project.create({ data: { name: 'dup', ownerId: user.id } });
    await expect(
      prisma.project.create({ data: { name: 'dup', ownerId: user.id } }),
    ).rejects.toThrow();
  });

  it('cascades delete when owner is deleted', async () => {
    const user = await createUser();
    await prisma.project.create({ data: { name: 'orphan', ownerId: user.id } });
    await prisma.user.delete({ where: { id: user.id } });
    const projects = await prisma.project.findMany({ where: { ownerId: user.id } });
    expect(projects).toHaveLength(0);
  });
});


// ── McpInstance model ──

describe('McpInstance', () => {
  it('creates an instance linked to server', async () => {
    const server = await createServer();
    const instance = await prisma.mcpInstance.create({
      data: { serverId: server.id },
    });
    expect(instance.status).toBe('STOPPED');
    expect(instance.serverId).toBe(server.id);
  });

  it('tracks instance status transitions', async () => {
    const server = await createServer();
    const instance = await prisma.mcpInstance.create({
      data: { serverId: server.id, status: 'STARTING' },
    });
    const running = await prisma.mcpInstance.update({
      where: { id: instance.id },
      data: { status: 'RUNNING', containerId: 'abc123', port: 8080 },
    });
    expect(running.status).toBe('RUNNING');
    expect(running.containerId).toBe('abc123');
    expect(running.port).toBe(8080);
  });

  it('cascades delete when server is deleted', async () => {
    const server = await createServer();
    await prisma.mcpInstance.create({ data: { serverId: server.id } });
    await prisma.mcpServer.delete({ where: { id: server.id } });
    const instances = await prisma.mcpInstance.findMany({ where: { serverId: server.id } });
    expect(instances).toHaveLength(0);
  });
});

// ── AuditLog model ──

describe('AuditLog', () => {
  it('creates an audit log entry', async () => {
    const user = await createUser();
    const log = await prisma.auditLog.create({
      data: {
        userId: user.id,
        action: 'CREATE',
        resource: 'McpServer',
        resourceId: 'server-123',
        details: { name: 'slack' },
      },
    });
    expect(log.action).toBe('CREATE');
    expect(log.resource).toBe('McpServer');
    expect(log.createdAt).toBeInstanceOf(Date);
  });

  it('supports querying by action and resource', async () => {
    const user = await createUser();
    await prisma.auditLog.createMany({
      data: [
        { userId: user.id, action: 'CREATE', resource: 'McpServer' },
        { userId: user.id, action: 'UPDATE', resource: 'McpServer' },
        { userId: user.id, action: 'CREATE', resource: 'Project' },
      ],
    });

    const creates = await prisma.auditLog.findMany({
      where: { action: 'CREATE' },
    });
    expect(creates).toHaveLength(2);

    const serverLogs = await prisma.auditLog.findMany({
      where: { resource: 'McpServer' },
    });
    expect(serverLogs).toHaveLength(2);
  });

  it('cascades delete when user is deleted', async () => {
    const user = await createUser();
    await prisma.auditLog.create({
      data: { userId: user.id, action: 'TEST', resource: 'Test' },
    });
    await prisma.user.delete({ where: { id: user.id } });
    const logs = await prisma.auditLog.findMany({ where: { userId: user.id } });
    expect(logs).toHaveLength(0);
  });
});

// ── User SSO fields ──

describe('User SSO fields', () => {
  it('stores provider and externalId', async () => {
    const user = await prisma.user.create({
      data: {
        email: 'sso@example.com',
        passwordHash: 'hash',
        provider: 'oidc',
        externalId: 'ext-123',
      },
    });
    expect(user.provider).toBe('oidc');
    expect(user.externalId).toBe('ext-123');
  });

  it('defaults provider and externalId to null', async () => {
    const user = await createUser();
    expect(user.provider).toBeNull();
    expect(user.externalId).toBeNull();
  });
});

// ── Group model ──

describe('Group', () => {
  it('creates a group with defaults', async () => {
    const group = await createGroup();
    expect(group.id).toBeDefined();
    expect(group.version).toBe(1);
  });

  it('enforces unique name', async () => {
    await createGroup({ name: 'devs' });
    await expect(createGroup({ name: 'devs' })).rejects.toThrow();
  });

  it('creates group members', async () => {
    const group = await createGroup();
    const user = await createUser();
    const member = await prisma.groupMember.create({
      data: { groupId: group.id, userId: user.id },
    });
    expect(member.groupId).toBe(group.id);
    expect(member.userId).toBe(user.id);
  });

  it('enforces unique group-user pair', async () => {
    const group = await createGroup();
    const user = await createUser();
    await prisma.groupMember.create({ data: { groupId: group.id, userId: user.id } });
    await expect(
      prisma.groupMember.create({ data: { groupId: group.id, userId: user.id } }),
    ).rejects.toThrow();
  });

  it('cascades delete when group is deleted', async () => {
    const group = await createGroup();
    const user = await createUser();
    await prisma.groupMember.create({ data: { groupId: group.id, userId: user.id } });
    await prisma.group.delete({ where: { id: group.id } });
    const members = await prisma.groupMember.findMany({ where: { groupId: group.id } });
    expect(members).toHaveLength(0);
  });
});

// ── RbacDefinition model ──

describe('RbacDefinition', () => {
  it('creates with defaults', async () => {
    const rbac = await prisma.rbacDefinition.create({
      data: { name: 'test-rbac' },
    });
    expect(rbac.subjects).toEqual([]);
    expect(rbac.roleBindings).toEqual([]);
    expect(rbac.version).toBe(1);
  });

  it('enforces unique name', async () => {
    await prisma.rbacDefinition.create({ data: { name: 'dup-rbac' } });
    await expect(prisma.rbacDefinition.create({ data: { name: 'dup-rbac' } })).rejects.toThrow();
  });

  it('stores subjects as JSON', async () => {
    const rbac = await prisma.rbacDefinition.create({
      data: {
        name: 'with-subjects',
        subjects: [{ kind: 'User', name: 'alice@test.com' }, { kind: 'Group', name: 'devs' }],
      },
    });
    const subjects = rbac.subjects as Array<{ kind: string; name: string }>;
    expect(subjects).toHaveLength(2);
    expect(subjects[0].kind).toBe('User');
  });

  it('stores roleBindings as JSON', async () => {
    const rbac = await prisma.rbacDefinition.create({
      data: {
        name: 'with-bindings',
        roleBindings: [{ role: 'editor', resource: 'servers' }],
      },
    });
    const bindings = rbac.roleBindings as Array<{ role: string; resource: string }>;
    expect(bindings).toHaveLength(1);
    expect(bindings[0].role).toBe('editor');
  });

  it('updates subjects and roleBindings', async () => {
    const rbac = await prisma.rbacDefinition.create({ data: { name: 'updatable-rbac' } });
    const updated = await prisma.rbacDefinition.update({
      where: { id: rbac.id },
      data: {
        subjects: [{ kind: 'User', name: 'bob@test.com' }],
        roleBindings: [{ role: 'admin', resource: '*' }],
      },
    });
    expect((updated.subjects as unknown[]).length).toBe(1);
    expect((updated.roleBindings as unknown[]).length).toBe(1);
  });
});

// ── ProjectServer model ──

describe('ProjectServer', () => {
  it('links project to server', async () => {
    const project = await createProject();
    const server = await createServer();
    const ps = await prisma.projectServer.create({
      data: { projectId: project.id, serverId: server.id },
    });
    expect(ps.projectId).toBe(project.id);
    expect(ps.serverId).toBe(server.id);
  });

  it('enforces unique project-server pair', async () => {
    const project = await createProject();
    const server = await createServer();
    await prisma.projectServer.create({ data: { projectId: project.id, serverId: server.id } });
    await expect(
      prisma.projectServer.create({ data: { projectId: project.id, serverId: server.id } }),
    ).rejects.toThrow();
  });

  it('cascades delete when project is deleted', async () => {
    const project = await createProject();
    const server = await createServer();
    await prisma.projectServer.create({ data: { projectId: project.id, serverId: server.id } });
    await prisma.project.delete({ where: { id: project.id } });
    const links = await prisma.projectServer.findMany({ where: { projectId: project.id } });
    expect(links).toHaveLength(0);
  });

  it('cascades delete when server is deleted', async () => {
    const project = await createProject();
    const server = await createServer();
    await prisma.projectServer.create({ data: { projectId: project.id, serverId: server.id } });
    await prisma.mcpServer.delete({ where: { id: server.id } });
    const links = await prisma.projectServer.findMany({ where: { serverId: server.id } });
    expect(links).toHaveLength(0);
  });
});


// ── Project new fields ──

describe('Project new fields', () => {
  it('stores llmProvider, llmModel', async () => {
    const user = await createUser();
    const project = await prisma.project.create({
      data: {
        name: 'filtered-project',
        ownerId: user.id,
        llmProvider: 'gemini-cli',
        llmModel: 'gemini-2.0-flash',
      },
    });
    expect(project.llmProvider).toBe('gemini-cli');
    expect(project.llmModel).toBe('gemini-2.0-flash');
  });

  it('defaults llmProvider and llmModel to null', async () => {
    const project = await createProject();
    expect(project.llmProvider).toBeNull();
    expect(project.llmModel).toBeNull();
  });
});

// ── seedTemplates ──

const testTemplates: SeedTemplate[] = [
  {
    name: 'github',
    version: '1.0.0',
    description: 'GitHub MCP server',
    packageName: '@anthropic/github-mcp',
    transport: 'STDIO',
    env: [{ name: 'GITHUB_TOKEN', description: 'Personal access token', required: true }],
  },
  {
    name: 'slack',
    version: '1.0.0',
    description: 'Slack MCP server',
    packageName: '@anthropic/slack-mcp',
    transport: 'STDIO',
    env: [],
  },
];

describe('seedTemplates', () => {
  it('seeds templates', async () => {
    const count = await seedTemplates(prisma, testTemplates);
    expect(count).toBe(2);

    const templates = await prisma.mcpTemplate.findMany({ orderBy: { name: 'asc' } });
    expect(templates).toHaveLength(2);
    expect(templates.map((t) => t.name)).toEqual(['github', 'slack']);
  });

  it('is idempotent (upsert)', async () => {
    await seedTemplates(prisma, testTemplates);
    const count = await seedTemplates(prisma, testTemplates);
    expect(count).toBe(2);

    const templates = await prisma.mcpTemplate.findMany();
    expect(templates).toHaveLength(2);
  });

  it('seeds env correctly', async () => {
    await seedTemplates(prisma, testTemplates);
    const github = await prisma.mcpTemplate.findUnique({ where: { name: 'github' } });
    const env = github!.env as Array<{ name: string; description?: string; required?: boolean }>;
    expect(env).toHaveLength(1);
    expect(env[0].name).toBe('GITHUB_TOKEN');
    expect(env[0].required).toBe(true);
  });

  it('accepts custom template list', async () => {
    const custom: SeedTemplate[] = [
      {
        name: 'custom-template',
        version: '2.0.0',
        description: 'Custom test template',
        packageName: '@test/custom',
        transport: 'STDIO',
        env: [],
      },
    ];
    const count = await seedTemplates(prisma, custom);
    expect(count).toBe(1);

    const templates = await prisma.mcpTemplate.findMany();
    expect(templates).toHaveLength(1);
    expect(templates[0].name).toBe('custom-template');
  });
});

}); // close 'db models' wrapper
