import { describe, it, expect, beforeEach, vi, type Mock } from 'vitest';
import { ImageEngine } from './index.js';
import type { CharacterDNA, MasterJSONPrompt } from '../../types/character.js';
import type { SceneTemplate } from '../../types/api.js';

// Mock dependencies
vi.mock('../../utils/logger.js', () => ({
  createModuleLogger: () => ({
    info: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
    debug: vi.fn(),
  }),
}));

vi.mock('../../utils/config.js', () => ({
  config: {
    webhookBaseUrl: 'https://example.com',
    dataDir: '/tmp/test-data',
  },
}));

const mockQueueAdd = vi.fn();
const mockQueueGetById = vi.fn();
const mockQueueUpdateStatus = vi.fn();
vi.mock('../queue-system/index.js', () => ({
  queue: {
    add: (...args: any[]) => mockQueueAdd(...args),
    getById: (...args: any[]) => mockQueueGetById(...args),
    updateStatus: (...args: any[]) => mockQueueUpdateStatus(...args),
  },
}));

const mockGetCharacter = vi.fn();
const mockGetMasterPrompt = vi.fn();
const mockGetTemplate = vi.fn();
const mockGetGeneratedImagesDir = vi.fn();
vi.mock('../data-store/index.js', () => ({
  dataStore: {
    getCharacter: (...args: any[]) => mockGetCharacter(...args),
    getMasterPrompt: (...args: any[]) => mockGetMasterPrompt(...args),
    getTemplate: (...args: any[]) => mockGetTemplate(...args),
    getGeneratedImagesDir: (...args: any[]) => mockGetGeneratedImagesDir(...args),
  },
}));

vi.mock('../character-engine/prompt-builder.js', () => ({
  buildSubjectDescription: vi.fn(() => 'a beautiful 25 year old model'),
  buildCameraSettings: vi.fn(() => 'shot on Canon EOS R5, 85mm lens'),
}));

vi.mock('./kie-client.js', () => ({
  KIEClient: vi.fn(),
  createKIEClient: vi.fn(() => ({})),
}));

vi.mock('./templates.js', () => ({
  TemplateManager: vi.fn(() => ({})),
}));

// --- Test fixtures ---

function makeDNA(overrides?: Partial<CharacterDNA>): CharacterDNA {
  return {
    id: 'char-1',
    createdAt: '2024-01-01T00:00:00Z',
    updatedAt: '2024-01-01T00:00:00Z',
    status: 'active',
    identity: {
      name: 'Test', age: 25, nationality: 'Turkish', city: 'Istanbul',
      occupation: 'Model', personality_tags: ['friendly'], mbti: 'ENFP', backstory: 'Test backstory',
    },
    physicalMeasurements: { height_cm: 170, weight_kg: 55, body_type: 'slim', measurements: { bust: 90, waist: 60, hips: 90 } },
    skinMap: { base: '#F5D0A9', shadow: '#D4A574', highlight: '#FFE4C4' },
    faceAnatomy: { eye_shape: 'almond', eye_color: '#8B4513', eyebrow_shape: 'arched', nose_type: 'straight', lip_shape: 'full', cheekbones: 'high', jawline: 'soft', distinctive_marks: [] },
    hairDNA: { style: 'wavy', length: 'long', texture: 'silky', root_color: '#3B2F2F', mid_color: '#5C4033', highlight_color: '#8B6914' },
    bodyBiometrics: { body_type_detail: 'hourglass', posture: 'upright', skin_texture: 'smooth' },
    profileView: { best_angle: 'three-quarter', signature_pose: 'hand on hip' },
    expressionMotion: { default_expression: 'soft smile', smile_type: 'gentle', gaze_style: 'direct' },
    makeupStyle: { philosophy: 'natural', skin_finish: 'dewy', colors: { primary: '#C48A6A', secondary: '#B5651D', accent: '#FF69B4' } },
    fashionStyle: { aesthetic: 'casual chic', main_colors: ['#FFFFFF', '#000000'], signature_pieces: ['white blouse', 'jeans'] },
    technicalSettings: { camera_body: 'Canon EOS R5', portrait_lens: '85mm f/1.4', full_body_lens: '35mm f/1.8', film_stock: 'Kodak Portra 400' },
    ...overrides,
  };
}

function makeMasterPrompt(overrides?: Partial<MasterJSONPrompt>): MasterJSONPrompt {
  return {
    characterId: 'char-1',
    subject: 'a beautiful 25 year old model',
    clothing: 'casual chic style, wearing white blouse, jeans',
    environment: 'three-quarter angle, hand on hip pose, professional studio setting',
    lighting: 'soft natural lighting, dewy skin finish',
    camera: 'shot on Canon EOS R5, 85mm f/1.4 portrait lens',
    aesthetic: 'natural makeup, casual chic fashion aesthetic',
    negative_prompt: 'blurry, low quality, distorted',
    createdAt: '2024-01-01T00:00:00Z',
    updatedAt: '2024-01-01T00:00:00Z',
    ...overrides,
  };
}

function makeTemplate(overrides?: Partial<SceneTemplate>): SceneTemplate {
  return {
    id: 'beach',
    name: 'Beach',
    category: 'outdoor',
    promptTemplate: '{subject}, walking on a tropical beach, {clothing}, {lighting}, ocean waves, {camera}',
    environment: { lighting: 'golden hour sunlight', weather: 'clear sky', timeOfDay: 'sunset' },
    defaultAspectRatio: '16:9',
    createdAt: '2024-01-01T00:00:00Z',
    updatedAt: '2024-01-01T00:00:00Z',
    ...overrides,
  };
}

describe('ImageEngine', () => {
  let engine: ImageEngine;

  beforeEach(() => {
    vi.clearAllMocks();
    engine = new ImageEngine({} as any, {} as any);
  });

  describe('buildFinalPrompt', () => {
    it('should replace all placeholders in the template', () => {
      const dna = makeDNA();
      const masterPrompt = makeMasterPrompt();
      const template = makeTemplate();

      const result = engine.buildFinalPrompt(dna, masterPrompt, template);

      expect(result).toContain('a beautiful 25 year old model');
      expect(result).toContain('casual chic style, wearing white blouse, jeans');
      expect(result).toContain('golden hour sunlight');
      expect(result).toContain('shot on Canon EOS R5, 85mm lens');
      expect(result).toContain('ocean waves');
    });

    it('should use template lighting over master prompt lighting', () => {
      const dna = makeDNA();
      const masterPrompt = makeMasterPrompt({ lighting: 'studio lighting' });
      const template = makeTemplate({
        environment: { lighting: 'neon lights', timeOfDay: 'night' },
      });

      const result = engine.buildFinalPrompt(dna, masterPrompt, template);
      expect(result).toContain('neon lights');
      expect(result).not.toContain('studio lighting');
    });

    it('should fall back to master prompt lighting when template lighting is empty', () => {
      const dna = makeDNA();
      const masterPrompt = makeMasterPrompt({ lighting: 'studio lighting' });
      const template = makeTemplate({
        environment: { lighting: '', timeOfDay: 'night' },
      });

      const result = engine.buildFinalPrompt(dna, masterPrompt, template);
      expect(result).toContain('studio lighting');
    });

    it('should handle template with {environment} placeholder', () => {
      const dna = makeDNA();
      const masterPrompt = makeMasterPrompt({ environment: 'urban rooftop' });
      const template = makeTemplate({
        promptTemplate: '{subject}, in {environment}, {camera}',
      });

      const result = engine.buildFinalPrompt(dna, masterPrompt, template);
      expect(result).toContain('urban rooftop');
    });

    it('should handle template with {aesthetic} placeholder', () => {
      const dna = makeDNA();
      const masterPrompt = makeMasterPrompt({ aesthetic: 'vintage film look' });
      const template = makeTemplate({
        promptTemplate: '{subject}, {aesthetic}, {camera}',
      });

      const result = engine.buildFinalPrompt(dna, masterPrompt, template);
      expect(result).toContain('vintage film look');
    });
  });

  describe('generateImage', () => {
    it('should queue an image generation task and return task ID', async () => {
      mockGetCharacter.mockResolvedValue(makeDNA());
      mockGetMasterPrompt.mockResolvedValue(makeMasterPrompt());
      mockGetTemplate.mockResolvedValue(makeTemplate());
      mockQueueAdd.mockResolvedValue({ id: 'queue-task-1', type: 'image_generation' });

      const taskId = await engine.generateImage('char-1', 'beach');

      expect(taskId).toBe('queue-task-1');
      expect(mockQueueAdd).toHaveBeenCalledWith(
        'image_generation',
        'char-1',
        expect.objectContaining({
          prompt: expect.any(String),
          negativePrompt: 'blurry, low quality, distorted',
          model: 'flux-2',
          aspectRatio: '16:9',
          templateId: 'beach',
          webhookUrl: 'https://example.com/api/v1/webhooks/kie',
        }),
      );
    });

    it('should use custom model and aspect ratio from options', async () => {
      mockGetCharacter.mockResolvedValue(makeDNA());
      mockGetMasterPrompt.mockResolvedValue(makeMasterPrompt());
      mockGetTemplate.mockResolvedValue(makeTemplate());
      mockQueueAdd.mockResolvedValue({ id: 'queue-task-2' });

      await engine.generateImage('char-1', 'beach', { model: '4o-image', aspectRatio: '9:16' });

      expect(mockQueueAdd).toHaveBeenCalledWith(
        'image_generation',
        'char-1',
        expect.objectContaining({
          model: '4o-image',
          aspectRatio: '9:16',
        }),
      );
    });

    it('should throw when character not found', async () => {
      mockGetCharacter.mockResolvedValue(null);

      await expect(engine.generateImage('missing', 'beach')).rejects.toThrow('Character not found: missing');
    });

    it('should throw when master prompt not found', async () => {
      mockGetCharacter.mockResolvedValue(makeDNA());
      mockGetMasterPrompt.mockResolvedValue(null);

      await expect(engine.generateImage('char-1', 'beach')).rejects.toThrow('Master prompt not found');
    });

    it('should throw when template not found', async () => {
      mockGetCharacter.mockResolvedValue(makeDNA());
      mockGetMasterPrompt.mockResolvedValue(makeMasterPrompt());
      mockGetTemplate.mockResolvedValue(null);

      await expect(engine.generateImage('char-1', 'missing-tpl')).rejects.toThrow('Template not found: missing-tpl');
    });
  });

  describe('handleWebhookResult', () => {
    it('should download image and mark task as completed', async () => {
      const mockBuffer = new ArrayBuffer(8);
      const mockResponse = { ok: true, arrayBuffer: vi.fn().mockResolvedValue(mockBuffer) };
      global.fetch = vi.fn().mockResolvedValue(mockResponse) as any;

      const { writeFile } = await import('fs/promises');
      vi.mock('fs/promises', () => ({ writeFile: vi.fn().mockResolvedValue(undefined) }));

      mockQueueGetById.mockReturnValue({ id: 'task-1', characterId: 'char-1' });
      mockGetGeneratedImagesDir.mockReturnValue('/tmp/test-data/characters/char-1/generated-images');
      mockQueueUpdateStatus.mockResolvedValue({});

      await engine.handleWebhookResult('task-1', 'https://cdn.example.com/image.jpg');

      expect(global.fetch).toHaveBeenCalledWith('https://cdn.example.com/image.jpg');
      expect(mockQueueUpdateStatus).toHaveBeenCalledWith('task-1', 'completed');
    });

    it('should skip processing when queue item not found', async () => {
      mockQueueGetById.mockReturnValue(undefined);

      await engine.handleWebhookResult('unknown-task', 'https://cdn.example.com/image.jpg');

      expect(mockQueueUpdateStatus).not.toHaveBeenCalled();
    });
  });

  describe('handleFailure', () => {
    it('should retry up to MAX_RETRIES times', async () => {
      // Set up retry tracking by calling generateImage first
      mockGetCharacter.mockResolvedValue(makeDNA());
      mockGetMasterPrompt.mockResolvedValue(makeMasterPrompt());
      mockGetTemplate.mockResolvedValue(makeTemplate());
      mockQueueAdd.mockResolvedValue({ id: 'task-1' });

      const taskId = await engine.generateImage('char-1', 'beach');

      // Now simulate failure - should trigger retry
      mockQueueAdd.mockResolvedValue({ id: 'task-2' });
      await engine.handleFailure(taskId, 'API error');

      // Should have called queue.add again for retry
      expect(mockQueueAdd).toHaveBeenCalledTimes(2);
    });

    it('should mark as failed after max retries exhausted', async () => {
      mockGetCharacter.mockResolvedValue(makeDNA());
      mockGetMasterPrompt.mockResolvedValue(makeMasterPrompt());
      mockGetTemplate.mockResolvedValue(makeTemplate());
      mockQueueAdd.mockResolvedValue({ id: 'task-1' });

      const taskId = await engine.generateImage('char-1', 'beach');

      // Exhaust retries
      for (let i = 0; i < 3; i++) {
        mockQueueAdd.mockResolvedValue({ id: `retry-${i}` });
        await engine.handleFailure(i === 0 ? taskId : `retry-${i - 1}`, 'API error');
      }

      // After 3 retries, the last failure should mark as failed
      mockQueueAdd.mockResolvedValue({ id: 'retry-3' });
      await engine.handleFailure('retry-2', 'API error');

      expect(mockQueueUpdateStatus).toHaveBeenCalledWith('retry-2', 'failed', 'API error');
    });

    it('should mark as failed immediately when no retry info exists', async () => {
      await engine.handleFailure('unknown-task', 'Some error');

      expect(mockQueueUpdateStatus).toHaveBeenCalledWith('unknown-task', 'failed', 'Some error');
    });
  });
});
