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

describe('Skill schema', () => {
  let prisma: PrismaClient;

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

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

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

  async function createUser() {
    return prisma.user.create({
      data: {
        email: `test-${Date.now()}-${Math.random()}@example.com`,
        name: 'Test',
        passwordHash: '!locked',
        role: 'USER',
      },
    });
  }

  async function createProject(name = `project-${Date.now()}-${Math.random()}`) {
    const user = await createUser();
    return prisma.project.create({ data: { name, ownerId: user.id } });
  }

  async function createAgent(name = `agent-${Date.now()}-${Math.random()}`) {
    const user = await createUser();
    const llm = await prisma.llm.create({
      data: { name: `llm-${Date.now()}-${Math.random()}`, type: 'openai', model: 'test' },
    });
    return prisma.agent.create({
      data: { name, llmId: llm.id, ownerId: user.id },
    });
  }

  it('creates a global skill (both FKs null) with defaults', async () => {
    const skill = await prisma.skill.create({
      data: { name: 'global-test', content: 'hello' },
    });
    expect(skill.id).toBeDefined();
    expect(skill.projectId).toBeNull();
    expect(skill.agentId).toBeNull();
    expect(skill.priority).toBe(5);
    expect(skill.semver).toBe('0.1.0');
    expect(skill.version).toBe(1);
    expect(skill.currentRevisionId).toBeNull();
    expect(skill.files).toEqual({});
    expect(skill.metadata).toEqual({});
    expect(skill.description).toBe('');
    expect(skill.summary).toBeNull();
    expect(skill.chapters).toBeNull();
  });

  it('creates a project-scoped skill', async () => {
    const project = await createProject();
    const skill = await prisma.skill.create({
      data: { name: 'proj-test', content: 'hi', projectId: project.id },
    });
    expect(skill.projectId).toBe(project.id);
    expect(skill.agentId).toBeNull();
  });

  it('creates an agent-scoped skill', async () => {
    const agent = await createAgent();
    const skill = await prisma.skill.create({
      data: { name: 'agent-test', content: 'hi', agentId: agent.id },
    });
    expect(skill.agentId).toBe(agent.id);
    expect(skill.projectId).toBeNull();
  });

  it('persists files and metadata as JSON', async () => {
    const skill = await prisma.skill.create({
      data: {
        name: 'with-files',
        content: 'hi',
        files: { 'scripts/setup.sh': '#!/bin/sh\necho hi' },
        metadata: {
          hooks: { PreToolUse: [{ command: 'echo' }] },
          postInstall: 'scripts/setup.sh',
        },
      },
    });
    expect(skill.files).toEqual({ 'scripts/setup.sh': '#!/bin/sh\necho hi' });
    expect(skill.metadata).toEqual({
      hooks: { PreToolUse: [{ command: 'echo' }] },
      postInstall: 'scripts/setup.sh',
    });
  });

  it('enforces unique (name, projectId)', async () => {
    const project = await createProject();
    await prisma.skill.create({
      data: { name: 'dup', content: 'a', projectId: project.id },
    });
    await expect(
      prisma.skill.create({
        data: { name: 'dup', content: 'b', projectId: project.id },
      }),
    ).rejects.toThrow();
  });

  it('enforces unique (name, agentId)', async () => {
    const agent = await createAgent();
    await prisma.skill.create({
      data: { name: 'dup', content: 'a', agentId: agent.id },
    });
    await expect(
      prisma.skill.create({
        data: { name: 'dup', content: 'b', agentId: agent.id },
      }),
    ).rejects.toThrow();
  });

  it('allows same name across different projects', async () => {
    const p1 = await createProject(`p1-${Date.now()}`);
    const p2 = await createProject(`p2-${Date.now()}`);
    await prisma.skill.create({
      data: { name: 'shared', content: 'a', projectId: p1.id },
    });
    const second = await prisma.skill.create({
      data: { name: 'shared', content: 'b', projectId: p2.id },
    });
    expect(second.id).toBeDefined();
  });

  it('allows same name across project + agent (different scopes)', async () => {
    const project = await createProject();
    const agent = await createAgent();
    await prisma.skill.create({
      data: { name: 'overlap', content: 'a', projectId: project.id },
    });
    const second = await prisma.skill.create({
      data: { name: 'overlap', content: 'b', agentId: agent.id },
    });
    expect(second.id).toBeDefined();
  });

  it('cascades on project delete', async () => {
    const project = await createProject();
    const skill = await prisma.skill.create({
      data: { name: 'cascade-me', content: 'hi', projectId: project.id },
    });
    await prisma.project.delete({ where: { id: project.id } });
    const found = await prisma.skill.findUnique({ where: { id: skill.id } });
    expect(found).toBeNull();
  });

  it('cascades on agent delete', async () => {
    const agent = await createAgent();
    const skill = await prisma.skill.create({
      data: { name: 'cascade-agent', content: 'hi', agentId: agent.id },
    });
    await prisma.agent.delete({ where: { id: agent.id } });
    const found = await prisma.skill.findUnique({ where: { id: skill.id } });
    expect(found).toBeNull();
  });

  it('preserves global skills when projects are deleted', async () => {
    const project = await createProject();
    const skill = await prisma.skill.create({
      data: { name: 'global-survives', content: 'hi' },
    });
    await prisma.project.delete({ where: { id: project.id } });
    const found = await prisma.skill.findUnique({ where: { id: skill.id } });
    expect(found).not.toBeNull();
    expect(found?.id).toBe(skill.id);
  });

  it('updates updatedAt on change', async () => {
    const skill = await prisma.skill.create({
      data: { name: 'mut', content: 'a' },
    });
    const original = skill.updatedAt;
    await new Promise((r) => setTimeout(r, 50));
    const updated = await prisma.skill.update({
      where: { id: skill.id },
      data: { content: 'b' },
    });
    expect(updated.updatedAt.getTime()).toBeGreaterThan(original.getTime());
  });
});
