import { describe, it, expect, beforeEach, vi } from 'vitest';
import Fastify, { FastifyInstance } from 'fastify';
import imageRoutes from './images.js';

// Stubs
function createMockImageEngine() {
  return {
    generateImage: vi.fn(),
  };
}

function createMockDataStore() {
  return {
    getCharacter: vi.fn(),
    getGeneratedImagesDir: vi.fn().mockReturnValue('/tmp/images'),
    getQualityReportsDir: vi.fn().mockReturnValue('/tmp/reports'),
  };
}

// Mock file-ops
vi.mock('../modules/data-store/file-ops.js', () => ({
  listFiles: vi.fn().mockResolvedValue([]),
  readJSON: vi.fn().mockResolvedValue(null),
  deleteFile: vi.fn().mockResolvedValue(true),
  fileExists: vi.fn().mockReturnValue(false),
}));

import { listFiles, readJSON, deleteFile, fileExists } from '../modules/data-store/file-ops.js';

describe('imageRoutes', () => {
  let app: FastifyInstance;
  let mockImageEngine: ReturnType<typeof createMockImageEngine>;
  let mockDataStore: ReturnType<typeof createMockDataStore>;

  beforeEach(async () => {
    vi.clearAllMocks();
    mockImageEngine = createMockImageEngine();
    mockDataStore = createMockDataStore();

    app = Fastify();
    await app.register(imageRoutes, {
      imageEngine: mockImageEngine as any,
      dataStore: mockDataStore as any,
    });
    await app.ready();
  });

  describe('POST /characters/:id/images/generate', () => {
    it('should return 201 when image generation is queued', async () => {
      mockDataStore.getCharacter.mockResolvedValue({ id: 'char-1' });

      // Mock dynamic imports used by the route
      vi.doMock('../modules/data-store/file-ops.js', () => ({
        listFiles: vi.fn().mockResolvedValue([]),
        readJSON: vi.fn().mockResolvedValue({ negative_prompt: 'blurry' }),
        deleteFile: vi.fn().mockResolvedValue(true),
        fileExists: vi.fn().mockReturnValue(false),
        ensureDir: vi.fn().mockResolvedValue(undefined),
      }));

      // This endpoint now requires prompt (not templateId) and does KIE + download
      // Since it uses dynamic imports internally, we test the validation path instead
      const res = await app.inject({
        method: 'POST',
        url: '/characters/char-1/images/generate',
        payload: {},
      });

      // Should return 400 because prompt is required
      expect(res.statusCode).toBe(400);
      expect(res.json().success).toBe(false);
      expect(res.json().error).toContain('prompt');
    });

    it('should return 404 when character not found', async () => {
      mockDataStore.getCharacter.mockResolvedValue(null);

      const res = await app.inject({
        method: 'POST',
        url: '/characters/missing/images/generate',
        payload: { prompt: 'test prompt' },
      });

      expect(res.statusCode).toBe(404);
      expect(res.json().success).toBe(false);
    });

    it('should return 400 when prompt is missing', async () => {
      mockDataStore.getCharacter.mockResolvedValue({ id: 'char-1' });

      const res = await app.inject({
        method: 'POST',
        url: '/characters/char-1/images/generate',
        payload: {},
      });

      expect(res.statusCode).toBe(400);
      expect(res.json().success).toBe(false);
      expect(res.json().error).toContain('prompt');
    });

    it('should return 400 when prompt is empty string', async () => {
      mockDataStore.getCharacter.mockResolvedValue({ id: 'char-1' });

      const res = await app.inject({
        method: 'POST',
        url: '/characters/char-1/images/generate',
        payload: { prompt: '  ' },
      });

      expect(res.statusCode).toBe(400);
      expect(res.json().success).toBe(false);
    });
  });

  describe('GET /characters/:id/images', () => {
    it('should return list of images', async () => {
      mockDataStore.getCharacter.mockResolvedValue({ id: 'char-1' });
      vi.mocked(listFiles).mockResolvedValue(['img-1.jpg', 'img-2.jpg']);

      const res = await app.inject({
        method: 'GET',
        url: '/characters/char-1/images',
      });

      expect(res.statusCode).toBe(200);
      const body = res.json();
      expect(body.success).toBe(true);
      expect(body.data).toHaveLength(2);
      // Images are sorted descending by imageId
      expect(body.data[0].imageId).toBe('img-2');
      expect(body.data[1].imageId).toBe('img-1');
    });

    it('should return 404 when character not found', async () => {
      mockDataStore.getCharacter.mockResolvedValue(null);

      const res = await app.inject({
        method: 'GET',
        url: '/characters/missing/images',
      });

      expect(res.statusCode).toBe(404);
    });

    it('should return empty array when no images exist', async () => {
      mockDataStore.getCharacter.mockResolvedValue({ id: 'char-1' });
      vi.mocked(listFiles).mockResolvedValue([]);

      const res = await app.inject({
        method: 'GET',
        url: '/characters/char-1/images',
      });

      expect(res.statusCode).toBe(200);
      expect(res.json().data).toEqual([]);
    });
  });

  describe('GET /characters/:id/images/:imageId', () => {
    it('should return image detail with quality report', async () => {
      mockDataStore.getCharacter.mockResolvedValue({ id: 'char-1' });
      vi.mocked(fileExists).mockReturnValue(true);
      vi.mocked(readJSON).mockResolvedValue({
        imageId: 'img-1',
        totalScore: 8.5,
        passed: true,
      });

      const res = await app.inject({
        method: 'GET',
        url: '/characters/char-1/images/img-1',
      });

      expect(res.statusCode).toBe(200);
      const body = res.json();
      expect(body.success).toBe(true);
      expect(body.data.imageId).toBe('img-1');
      expect(body.data.qualityReport.totalScore).toBe(8.5);
    });

    it('should return null qualityReport when no report exists', async () => {
      mockDataStore.getCharacter.mockResolvedValue({ id: 'char-1' });
      vi.mocked(fileExists).mockReturnValue(true);
      vi.mocked(readJSON).mockResolvedValue(null);

      const res = await app.inject({
        method: 'GET',
        url: '/characters/char-1/images/img-1',
      });

      expect(res.statusCode).toBe(200);
      expect(res.json().data.qualityReport).toBeNull();
    });

    it('should return 404 when image not found', async () => {
      mockDataStore.getCharacter.mockResolvedValue({ id: 'char-1' });
      vi.mocked(fileExists).mockReturnValue(false);

      const res = await app.inject({
        method: 'GET',
        url: '/characters/char-1/images/nonexistent',
      });

      expect(res.statusCode).toBe(404);
      expect(res.json().error).toContain('Image not found');
    });

    it('should return 404 when character not found', async () => {
      mockDataStore.getCharacter.mockResolvedValue(null);

      const res = await app.inject({
        method: 'GET',
        url: '/characters/missing/images/img-1',
      });

      expect(res.statusCode).toBe(404);
    });
  });

  describe('DELETE /characters/:id/images/:imageId', () => {
    it('should delete image and quality report', async () => {
      mockDataStore.getCharacter.mockResolvedValue({ id: 'char-1' });
      vi.mocked(fileExists).mockReturnValue(true);
      vi.mocked(deleteFile).mockResolvedValue(true);

      const res = await app.inject({
        method: 'DELETE',
        url: '/characters/char-1/images/img-1',
      });

      expect(res.statusCode).toBe(200);
      const body = res.json();
      expect(body.success).toBe(true);
      expect(body.data.imageId).toBe('img-1');
      expect(body.data.deleted).toBe(true);
      // Should delete both image and quality report
      expect(deleteFile).toHaveBeenCalledTimes(2);
    });

    it('should return 404 when image not found', async () => {
      mockDataStore.getCharacter.mockResolvedValue({ id: 'char-1' });
      vi.mocked(fileExists).mockReturnValue(false);

      const res = await app.inject({
        method: 'DELETE',
        url: '/characters/char-1/images/nonexistent',
      });

      expect(res.statusCode).toBe(404);
    });

    it('should return 404 when character not found', async () => {
      mockDataStore.getCharacter.mockResolvedValue(null);

      const res = await app.inject({
        method: 'DELETE',
        url: '/characters/missing/images/img-1',
      });

      expect(res.statusCode).toBe(404);
    });
  });
});
