import { describe, it, expect, vi } from 'vitest';
import { PromptService } from '../../src/services/prompt.service.js';
import type { IPromptRepository } from '../../src/repositories/prompt.repository.js';
import type { IPromptRequestRepository } from '../../src/repositories/prompt-request.repository.js';
import type { IProjectRepository } from '../../src/repositories/project.repository.js';
import type { IAgentRepository } from '../../src/repositories/agent.repository.js';
import type { Prompt, Agent } from '@prisma/client';

/**
 * Coverage for the new "Prompt.agentId" path: creating a prompt scoped to an
 * agent, listing prompts by agent, XOR-with-project enforcement at the schema
 * level, and the upsert path that resolves agent name → id.
 */

function makePrompt(overrides: Partial<Prompt> = {}): Prompt {
  return {
    id: 'prompt-1',
    name: 'p',
    content: 'c',
    projectId: null,
    agentId: null,
    priority: 5,
    summary: null,
    chapters: null,
    linkTarget: null,
    version: 1,
    createdAt: new Date(),
    updatedAt: new Date(),
    ...overrides,
  };
}

function makeAgent(overrides: Partial<Agent> = {}): Agent {
  return {
    id: 'agent-1',
    name: 'reviewer',
    description: '',
    systemPrompt: '',
    llmId: 'llm-1',
    projectId: null,
    defaultPersonalityId: null,
    proxyModelName: null,
    defaultParams: {} as Agent['defaultParams'],
    extras: {} as Agent['extras'],
    ownerId: 'owner-1',
    version: 1,
    createdAt: new Date(),
    updatedAt: new Date(),
    ...overrides,
  };
}

function mockPromptRepo(): IPromptRepository {
  const rows = new Map<string, Prompt>();
  return {
    findAll: vi.fn(async () => [...rows.values()]),
    findGlobal: vi.fn(async () =>
      [...rows.values()].filter((p) => p.projectId === null && p.agentId === null)),
    findByAgent: vi.fn(async (agentId: string) =>
      [...rows.values()].filter((p) => p.agentId === agentId)),
    findById: vi.fn(async (id: string) => rows.get(id) ?? null),
    findByNameAndProject: vi.fn(async (name: string, projectId: string | null) => {
      for (const p of rows.values()) {
        if (p.name === name && (p.projectId ?? null) === projectId) return p;
      }
      return null;
    }),
    findByNameAndAgent: vi.fn(async (name: string, agentId: string | null) => {
      for (const p of rows.values()) {
        if (p.name === name && (p.agentId ?? null) === agentId) return p;
      }
      return null;
    }),
    create: vi.fn(async (data) => {
      const row = makePrompt({
        id: `prompt-${rows.size + 1}`,
        name: data.name,
        content: data.content,
        projectId: data.projectId ?? null,
        agentId: data.agentId ?? null,
        priority: data.priority ?? 5,
        linkTarget: data.linkTarget ?? null,
      });
      rows.set(row.id, row);
      return row;
    }),
    update: vi.fn(async (id, data) => {
      const existing = rows.get(id);
      if (!existing) throw new Error('not found');
      const next = { ...existing, ...data } as Prompt;
      rows.set(id, next);
      return next;
    }),
    delete: vi.fn(async (id: string) => { rows.delete(id); }),
  };
}

function mockPromptRequestRepo(): IPromptRequestRepository {
  return {
    findAll: vi.fn(async () => []),
    findGlobal: vi.fn(async () => []),
    findById: vi.fn(async () => null),
    findByNameAndProject: vi.fn(async () => null),
    findBySession: vi.fn(async () => []),
    create: vi.fn(),
    update: vi.fn(),
    delete: vi.fn(),
  };
}

function mockProjectRepo(): IProjectRepository {
  return {
    findAll: vi.fn(async () => []),
    findById: vi.fn(async () => null),
    findByName: vi.fn(async () => null),
    create: vi.fn(),
    update: vi.fn(),
    delete: vi.fn(),
  };
}

function mockAgentRepo(initial: Agent[]): IAgentRepository {
  const rows = new Map<string, Agent>(initial.map((a) => [a.id, a]));
  return {
    findAll: vi.fn(async () => [...rows.values()]),
    findById: vi.fn(async (id: string) => rows.get(id) ?? null),
    findByName: vi.fn(async (name: string) => {
      for (const a of rows.values()) if (a.name === name) return a;
      return null;
    }),
    findByProjectId: vi.fn(async () => []),
    create: vi.fn(),
    update: vi.fn(),
    delete: vi.fn(),
  };
}

describe('PromptService — agent-direct scope', () => {
  it('creates a prompt directly attached to an agent', async () => {
    const promptRepo = mockPromptRepo();
    const agentRepo = mockAgentRepo([makeAgent()]);
    const service = new PromptService(
      promptRepo,
      mockPromptRequestRepo(),
      mockProjectRepo(),
      undefined,
      agentRepo,
    );

    const prompt = await service.createPrompt({
      name: 'agent-only',
      content: 'hi',
      agentId: 'agent-1',
    });

    expect(prompt.agentId).toBe('agent-1');
    expect(prompt.projectId).toBeNull();
  });

  it('rejects create when both projectId and agentId are set', async () => {
    const service = new PromptService(
      mockPromptRepo(),
      mockPromptRequestRepo(),
      mockProjectRepo(),
      undefined,
      mockAgentRepo([makeAgent()]),
    );

    await expect(
      service.createPrompt({
        name: 'bad',
        content: 'c',
        projectId: 'proj-1',
        agentId: 'agent-1',
      }),
    ).rejects.toThrow();
  });

  it('rejects create when the agent does not exist', async () => {
    const service = new PromptService(
      mockPromptRepo(),
      mockPromptRequestRepo(),
      mockProjectRepo(),
      undefined,
      mockAgentRepo([]),
    );

    await expect(
      service.createPrompt({ name: 'orphan', content: 'c', agentId: 'agent-ghost' }),
    ).rejects.toThrow(/Agent not found/);
  });

  it('refuses agent-scoped create when AgentRepository is not wired', async () => {
    // Mirrors the "agentRepo: undefined" wiring that older callers may use.
    const service = new PromptService(
      mockPromptRepo(),
      mockPromptRequestRepo(),
      mockProjectRepo(),
    );

    await expect(
      service.createPrompt({ name: 'oops', content: 'c', agentId: 'agent-1' }),
    ).rejects.toThrow(/AgentRepository/);
  });

  it('lists prompts directly attached to an agent', async () => {
    const promptRepo = mockPromptRepo();
    const agentRepo = mockAgentRepo([makeAgent()]);
    const service = new PromptService(
      promptRepo,
      mockPromptRequestRepo(),
      mockProjectRepo(),
      undefined,
      agentRepo,
    );

    await service.createPrompt({ name: 'a', content: 'A', agentId: 'agent-1' });
    await service.createPrompt({ name: 'b', content: 'B', agentId: 'agent-1' });
    // Different agent — should NOT show up.
    const otherAgentRepo = mockAgentRepo([makeAgent({ id: 'agent-other', name: 'other' })]);
    const otherService = new PromptService(
      promptRepo,
      mockPromptRequestRepo(),
      mockProjectRepo(),
      undefined,
      otherAgentRepo,
    );
    await otherService.createPrompt({ name: 'c', content: 'C', agentId: 'agent-other' });

    const list = await service.listPromptsForAgent('agent-1');
    expect(list.map((p) => p.name).sort()).toEqual(['a', 'b']);
  });

  it('upserts by name with agent scope', async () => {
    const promptRepo = mockPromptRepo();
    const agentRepo = mockAgentRepo([makeAgent()]);
    const service = new PromptService(
      promptRepo,
      mockPromptRequestRepo(),
      mockProjectRepo(),
      undefined,
      agentRepo,
    );

    const first = await service.upsertByName({
      name: 'tone',
      content: 'be polite',
      agent: 'reviewer',
    });
    expect(first.agentId).toBe('agent-1');

    const second = await service.upsertByName({
      name: 'tone',
      content: 'be terse',
      agent: 'reviewer',
    });
    expect(second.id).toBe(first.id);
    expect(second.content).toBe('be terse');
  });

  it('upsert rejects when both project and agent are provided', async () => {
    // Project must resolve (otherwise the service throws "Project not found"
    // before reaching the XOR check). Make findByName return a project.
    const projectRepo = mockProjectRepo();
    vi.mocked(projectRepo.findByName).mockResolvedValue({
      id: 'proj-1', name: 'p',
    } as Awaited<ReturnType<typeof projectRepo.findByName>>);

    const service = new PromptService(
      mockPromptRepo(),
      mockPromptRequestRepo(),
      projectRepo,
      undefined,
      mockAgentRepo([makeAgent()]),
    );

    await expect(
      service.upsertByName({
        name: 'x',
        content: 'c',
        project: 'p',
        agent: 'reviewer',
      }),
    ).rejects.toThrow(/XOR/);
  });
});
