import { describe, it, expect, vi, beforeEach } 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 { Prompt, PromptRequest, Project } from '@prisma/client';

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

function makePromptRequest(overrides: Partial<PromptRequest> = {}): PromptRequest {
  return {
    id: 'req-1',
    name: 'test-request',
    content: 'Proposed content',
    projectId: null,
    priority: 5,
    createdBySession: 'session-abc',
    createdByUserId: null,
    createdAt: new Date(),
    ...overrides,
  };
}

function makeProject(overrides: Partial<Project> = {}): Project {
  return {
    id: 'proj-1',
    name: 'test-project',
    description: '',
    prompt: '',
    proxyModel: '',
    gated: true,
    llmProvider: null,
    llmModel: null,
    ownerId: 'user-1',
    createdAt: new Date(),
    updatedAt: new Date(),
    ...overrides,
  } as Project;
}

function mockPromptRepo(): IPromptRepository {
  return {
    findAll: vi.fn(async () => []),
    findGlobal: vi.fn(async () => []),
    findById: vi.fn(async () => null),
    findByNameAndProject: vi.fn(async () => null),
    create: vi.fn(async (data) => makePrompt(data)),
    update: vi.fn(async (id, data) => makePrompt({ id, ...data })),
    delete: vi.fn(async () => {}),
  };
}

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(async (data) => makePromptRequest(data)),
    update: vi.fn(async (id, data) => makePromptRequest({ id, ...data })),
    delete: vi.fn(async () => {}),
  };
}

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

describe('PromptService', () => {
  let promptRepo: IPromptRepository;
  let promptRequestRepo: IPromptRequestRepository;
  let projectRepo: IProjectRepository;
  let service: PromptService;

  beforeEach(() => {
    promptRepo = mockPromptRepo();
    promptRequestRepo = mockPromptRequestRepo();
    projectRepo = mockProjectRepo();
    service = new PromptService(promptRepo, promptRequestRepo, projectRepo);
  });

  // ── Prompt CRUD ──

  describe('listPrompts', () => {
    it('should return all prompts', async () => {
      const prompts = [makePrompt(), makePrompt({ id: 'prompt-2', name: 'other' })];
      vi.mocked(promptRepo.findAll).mockResolvedValue(prompts);

      const result = await service.listPrompts();
      expect(result).toEqual(prompts);
      expect(promptRepo.findAll).toHaveBeenCalledWith(undefined);
    });

    it('should filter by projectId', async () => {
      await service.listPrompts('proj-1');
      expect(promptRepo.findAll).toHaveBeenCalledWith('proj-1');
    });
  });

  describe('listGlobalPrompts', () => {
    it('should return only global prompts', async () => {
      const globalPrompts = [makePrompt({ name: 'global-rule', projectId: null })];
      vi.mocked(promptRepo.findGlobal).mockResolvedValue(globalPrompts);

      const result = await service.listGlobalPrompts();
      expect(result).toEqual(globalPrompts);
      expect(promptRepo.findGlobal).toHaveBeenCalled();
    });
  });

  describe('getPrompt', () => {
    it('should return a prompt by id', async () => {
      const prompt = makePrompt();
      vi.mocked(promptRepo.findById).mockResolvedValue(prompt);

      const result = await service.getPrompt('prompt-1');
      expect(result).toEqual(prompt);
    });

    it('should throw NotFoundError for missing prompt', async () => {
      await expect(service.getPrompt('nope')).rejects.toThrow('Prompt not found: nope');
    });
  });

  describe('createPrompt', () => {
    it('should create a prompt', async () => {
      const result = await service.createPrompt({ name: 'new-prompt', content: 'stuff' });
      expect(promptRepo.create).toHaveBeenCalledWith({ name: 'new-prompt', content: 'stuff' });
      expect(result.name).toBe('new-prompt');
    });

    it('should validate project exists when projectId given', async () => {
      vi.mocked(projectRepo.findById).mockResolvedValue(makeProject());
      await service.createPrompt({ name: 'scoped', content: 'x', projectId: 'proj-1' });
      expect(projectRepo.findById).toHaveBeenCalledWith('proj-1');
    });

    it('should throw when project not found', async () => {
      await expect(
        service.createPrompt({ name: 'bad', content: 'x', projectId: 'nope' }),
      ).rejects.toThrow('Project not found: nope');
    });

    it('should reject invalid name format', async () => {
      await expect(
        service.createPrompt({ name: 'INVALID_NAME', content: 'x' }),
      ).rejects.toThrow();
    });
  });

  describe('updatePrompt', () => {
    it('should update prompt content', async () => {
      vi.mocked(promptRepo.findById).mockResolvedValue(makePrompt());
      await service.updatePrompt('prompt-1', { content: 'updated' });
      // Auto-patch bump on content change (PR-2): updatePrompt now also
      // emits the new semver in the same update call.
      expect(promptRepo.update).toHaveBeenCalledWith('prompt-1', { content: 'updated', semver: '0.1.1' });
    });

    it('should throw for missing prompt', async () => {
      await expect(service.updatePrompt('nope', { content: 'x' })).rejects.toThrow('Prompt not found');
    });
  });

  describe('deletePrompt', () => {
    it('should delete an existing prompt', async () => {
      vi.mocked(promptRepo.findById).mockResolvedValue(makePrompt());
      await service.deletePrompt('prompt-1');
      expect(promptRepo.delete).toHaveBeenCalledWith('prompt-1');
    });

    it('should throw for missing prompt', async () => {
      await expect(service.deletePrompt('nope')).rejects.toThrow('Prompt not found');
    });

    it('should reset system prompts to default on delete', async () => {
      vi.mocked(promptRepo.findById).mockResolvedValue(makePrompt({ name: 'gate-instructions', projectId: 'sys-proj' }));
      vi.mocked(projectRepo.findById).mockResolvedValue(makeProject({ id: 'sys-proj', name: 'mcpctl-system' }));

      const result = await service.deletePrompt('prompt-1');
      // Should reset via update, not delete
      expect(promptRepo.update).toHaveBeenCalledWith('prompt-1', expect.objectContaining({ content: expect.any(String) }));
      expect(promptRepo.delete).not.toHaveBeenCalled();
      expect(result).toBeDefined();
    });

    it('should allow deletion of non-system project prompts', async () => {
      vi.mocked(promptRepo.findById).mockResolvedValue(makePrompt({ projectId: 'proj-1' }));
      vi.mocked(projectRepo.findById).mockResolvedValue(makeProject({ id: 'proj-1', name: 'my-project' }));

      await service.deletePrompt('prompt-1');
      expect(promptRepo.delete).toHaveBeenCalledWith('prompt-1');
    });
  });

  // ── PromptRequest CRUD ──

  describe('listPromptRequests', () => {
    it('should return all prompt requests', async () => {
      const reqs = [makePromptRequest()];
      vi.mocked(promptRequestRepo.findAll).mockResolvedValue(reqs);

      const result = await service.listPromptRequests();
      expect(result).toEqual(reqs);
    });
  });

  describe('getPromptRequest', () => {
    it('should return a prompt request by id', async () => {
      const req = makePromptRequest();
      vi.mocked(promptRequestRepo.findById).mockResolvedValue(req);

      const result = await service.getPromptRequest('req-1');
      expect(result).toEqual(req);
    });

    it('should throw for missing request', async () => {
      await expect(service.getPromptRequest('nope')).rejects.toThrow('PromptRequest not found');
    });
  });

  describe('deletePromptRequest', () => {
    it('should delete an existing request', async () => {
      vi.mocked(promptRequestRepo.findById).mockResolvedValue(makePromptRequest());
      await service.deletePromptRequest('req-1');
      expect(promptRequestRepo.delete).toHaveBeenCalledWith('req-1');
    });
  });

  // ── Propose ──

  describe('propose', () => {
    it('should create a prompt request', async () => {
      const result = await service.propose({
        name: 'my-prompt',
        content: 'proposal',
        createdBySession: 'sess-1',
      });
      expect(promptRequestRepo.create).toHaveBeenCalledWith(
        expect.objectContaining({ name: 'my-prompt', content: 'proposal', createdBySession: 'sess-1' }),
      );
      expect(result.name).toBe('my-prompt');
    });

    it('should validate project exists when projectId given', async () => {
      vi.mocked(projectRepo.findById).mockResolvedValue(makeProject());
      await service.propose({
        name: 'scoped',
        content: 'x',
        projectId: 'proj-1',
      });
      expect(projectRepo.findById).toHaveBeenCalledWith('proj-1');
    });
  });

  // ── Approve ──

  describe('approve', () => {
    it('should delete request and create prompt (atomic)', async () => {
      const req = makePromptRequest({ id: 'req-1', name: 'approved', content: 'good stuff', projectId: 'proj-1' });
      vi.mocked(promptRequestRepo.findById).mockResolvedValue(req);

      const result = await service.approve('req-1');

      expect(promptRepo.create).toHaveBeenCalledWith(
        expect.objectContaining({ name: 'approved', content: 'good stuff', projectId: 'proj-1' }),
      );
      expect(promptRequestRepo.delete).toHaveBeenCalledWith('req-1');
      expect(result.name).toBe('approved');
    });

    it('should throw for missing request', async () => {
      await expect(service.approve('nope')).rejects.toThrow('PromptRequest not found');
    });

    it('should handle global prompt (no projectId)', async () => {
      const req = makePromptRequest({ id: 'req-2', name: 'global', content: 'stuff', projectId: null });
      vi.mocked(promptRequestRepo.findById).mockResolvedValue(req);

      await service.approve('req-2');

      // Should NOT include projectId in the create call
      const createArg = vi.mocked(promptRepo.create).mock.calls[0]![0];
      expect(createArg).not.toHaveProperty('projectId');
    });
  });

  // ── Priority ──

  describe('prompt priority', () => {
    it('creates prompt with explicit priority', async () => {
      const result = await service.createPrompt({ name: 'high-pri', content: 'x', priority: 8 });
      expect(promptRepo.create).toHaveBeenCalledWith(expect.objectContaining({ priority: 8 }));
      expect(result.priority).toBe(8);
    });

    it('uses default priority 5 when not specified', async () => {
      const result = await service.createPrompt({ name: 'default-pri', content: 'x' });
      // Default in schema is 5 — create is called without priority
      const createArg = vi.mocked(promptRepo.create).mock.calls[0]![0];
      expect(createArg.priority).toBeUndefined();
    });

    it('rejects priority below 1', async () => {
      await expect(
        service.createPrompt({ name: 'bad-pri', content: 'x', priority: 0 }),
      ).rejects.toThrow();
    });

    it('rejects priority above 10', async () => {
      await expect(
        service.createPrompt({ name: 'bad-pri', content: 'x', priority: 11 }),
      ).rejects.toThrow();
    });

    it('updates prompt priority', async () => {
      vi.mocked(promptRepo.findById).mockResolvedValue(makePrompt());
      await service.updatePrompt('prompt-1', { priority: 3 });
      expect(promptRepo.update).toHaveBeenCalledWith('prompt-1', expect.objectContaining({ priority: 3 }));
    });
  });

  // ── Link Target ──

  describe('prompt links', () => {
    it('creates linked prompt with valid linkTarget', async () => {
      const result = await service.createPrompt({
        name: 'linked',
        content: 'link content',
        linkTarget: 'other-project/docmost-mcp:docmost://pages/abc',
      });
      expect(promptRepo.create).toHaveBeenCalledWith(
        expect.objectContaining({ linkTarget: 'other-project/docmost-mcp:docmost://pages/abc' }),
      );
    });

    it('rejects invalid link format', async () => {
      await expect(
        service.createPrompt({ name: 'bad-link', content: 'x', linkTarget: 'invalid-format' }),
      ).rejects.toThrow();
    });

    it('rejects link without server part', async () => {
      await expect(
        service.createPrompt({ name: 'bad-link', content: 'x', linkTarget: 'project:uri' }),
      ).rejects.toThrow();
    });

    it('approve carries priority from request to prompt', async () => {
      const req = makePromptRequest({ id: 'req-1', name: 'high-pri', content: 'x', projectId: 'proj-1', priority: 9 });
      vi.mocked(promptRequestRepo.findById).mockResolvedValue(req);

      await service.approve('req-1');

      expect(promptRepo.create).toHaveBeenCalledWith(
        expect.objectContaining({ priority: 9 }),
      );
    });

    it('propose passes priority through', async () => {
      const result = await service.propose({
        name: 'pri-req',
        content: 'x',
        priority: 7,
      });
      expect(promptRequestRepo.create).toHaveBeenCalledWith(
        expect.objectContaining({ priority: 7 }),
      );
    });
  });

  // ── Visibility ──

  describe('getVisiblePrompts', () => {
    it('should return approved prompts and session requests', async () => {
      vi.mocked(promptRepo.findAll).mockResolvedValue([
        makePrompt({ name: 'approved-1', content: 'A' }),
      ]);
      vi.mocked(promptRequestRepo.findBySession).mockResolvedValue([
        makePromptRequest({ name: 'pending-1', content: 'B' }),
      ]);

      const result = await service.getVisiblePrompts('proj-1', 'sess-1');

      expect(result).toHaveLength(2);
      expect(result[0]).toMatchObject({ name: 'approved-1', content: 'A', type: 'prompt' });
      expect(result[1]).toMatchObject({ name: 'pending-1', content: 'B', type: 'promptrequest' });
    });

    it('should not include pending requests without sessionId', async () => {
      vi.mocked(promptRepo.findAll).mockResolvedValue([makePrompt()]);

      const result = await service.getVisiblePrompts('proj-1');

      expect(result).toHaveLength(1);
      expect(promptRequestRepo.findBySession).not.toHaveBeenCalled();
    });

    it('should return empty when no prompts or requests', async () => {
      const result = await service.getVisiblePrompts();
      expect(result).toEqual([]);
    });
  });
});
