import { describe, it, expect, vi, beforeEach } from 'vitest';
import { QualityController } from './quality-control.js';
import type { QualityReport } from '../../types/api.js';
import type { CharacterDNA } from '../../types/character.js';

// --- Mocks ---

const mockDna: CharacterDNA = {
  id: 'char-1',
  createdAt: '2025-01-01T00:00:00Z',
  updatedAt: '2025-01-01T00:00:00Z',
  status: 'active',
  identity: { name: 'Test', age: 25, nationality: 'TR', city: 'Istanbul', occupation: 'Model', personality_tags: [], mbti: 'ENFP', backstory: '' },
  physicalMeasurements: { height_cm: 170, weight_kg: 55, body_type: 'slim', measurements: { bust: 85, 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: 'ectomorph', posture: 'upright', skin_texture: 'smooth' },
  profileView: { best_angle: 'left-45', signature_pose: 'hand-on-hip' },
  expressionMotion: { default_expression: 'soft-smile', smile_type: 'closed-lip', gaze_style: 'direct' },
  makeupStyle: { philosophy: 'natural', skin_finish: 'dewy', colors: { primary: '#C48A6A', secondary: '#B5651D', accent: '#FF6347' } },
  fashionStyle: { aesthetic: 'casual-chic', main_colors: ['#FFFFFF', '#000000'], signature_pieces: ['denim jacket'] },
  technicalSettings: { camera_body: 'Sony A7IV', portrait_lens: '85mm f/1.4', full_body_lens: '35mm f/1.8', film_stock: 'Portra 400' },
};

function makeReport(overrides: Partial<QualityReport> = {}): QualityReport {
  return {
    imageId: 'img-1',
    characterId: 'char-1',
    taskId: 'task-1',
    scores: { characterAccuracy: 8, bodyConsistency: 7, environmentQuality: 7, overallAesthetic: 8 },
    totalScore: 7.6,
    passed: true,
    feedback: 'Good quality',
    createdAt: new Date().toISOString(),
    ...overrides,
  };
}

const mockAnalyzeImage = vi.fn<any>();
const mockVlm = { analyzeImage: mockAnalyzeImage } as any;

vi.mock('../data-store/index.js', () => ({
  dataStore: {
    getCharacter: vi.fn(),
    getQualityReportsDir: vi.fn(),
  },
}));

vi.mock('../data-store/file-ops.js', () => ({
  atomicWriteJSON: vi.fn(),
}));

import { dataStore } from '../data-store/index.js';
import { atomicWriteJSON } from '../data-store/file-ops.js';

const mockedDataStore = vi.mocked(dataStore);
const mockedAtomicWrite = vi.mocked(atomicWriteJSON);

beforeEach(() => {
  vi.clearAllMocks();
  mockedDataStore.getCharacter.mockResolvedValue(mockDna);
  mockedDataStore.getQualityReportsDir.mockReturnValue('/data/characters/char-1/quality-reports');
});

describe('QualityController', () => {
  describe('checkQuality', () => {
    it('should run VLM analysis and save the report', async () => {
      const report = makeReport();
      mockAnalyzeImage.mockResolvedValue(report);

      const controller = new QualityController(mockVlm);
      const result = await controller.checkQuality('http://img.test/1.jpg', 'char-1', 'img-1', 'task-1');

      expect(mockAnalyzeImage).toHaveBeenCalledWith('http://img.test/1.jpg', mockDna, 'img-1', 'task-1', 7.0);
      expect(mockedAtomicWrite).toHaveBeenCalledTimes(1);
      const writtenPath = mockedAtomicWrite.mock.calls[0][0] as string;
      expect(writtenPath).toContain('quality-reports');
      expect(writtenPath).toContain('img-1.json');
      expect(mockedAtomicWrite.mock.calls[0][1]).toEqual(report);
      expect(result).toEqual(report);
    });

    it('should use custom threshold when provided', async () => {
      const report = makeReport({ totalScore: 5.0, passed: false });
      mockAnalyzeImage.mockResolvedValue(report);

      const controller = new QualityController(mockVlm);
      await controller.checkQuality('http://img.test/1.jpg', 'char-1', 'img-1', 'task-1', 8.0);

      expect(mockAnalyzeImage).toHaveBeenCalledWith('http://img.test/1.jpg', mockDna, 'img-1', 'task-1', 8.0);
    });

    it('should throw if character not found', async () => {
      mockedDataStore.getCharacter.mockResolvedValue(null);

      const controller = new QualityController(mockVlm);
      await expect(
        controller.checkQuality('http://img.test/1.jpg', 'missing', 'img-1', 'task-1'),
      ).rejects.toThrow('Character not found: missing');
    });
  });

  describe('checkWithRetry', () => {
    it('should return immediately if first check passes', async () => {
      const report = makeReport({ passed: true, totalScore: 8.5 });
      mockAnalyzeImage.mockResolvedValue(report);

      const regenerateFn = vi.fn();
      const controller = new QualityController(mockVlm);
      const result = await controller.checkWithRetry(
        'http://img.test/1.jpg', 'char-1', 'img-1', 'task-1', regenerateFn,
      );

      expect(result.passed).toBe(true);
      expect(regenerateFn).not.toHaveBeenCalled();
      expect(mockAnalyzeImage).toHaveBeenCalledTimes(1);
    });

    it('should retry with regeneration when below threshold', async () => {
      const failReport = makeReport({ passed: false, totalScore: 5.0, feedback: 'Hair color mismatch' });
      const passReport = makeReport({ passed: true, totalScore: 8.0 });
      mockAnalyzeImage.mockResolvedValueOnce(failReport).mockResolvedValueOnce(passReport);

      const regenerateFn = vi.fn().mockResolvedValue('http://img.test/2.jpg');
      const controller = new QualityController(mockVlm);
      const result = await controller.checkWithRetry(
        'http://img.test/1.jpg', 'char-1', 'img-1', 'task-1', regenerateFn,
      );

      expect(result.passed).toBe(true);
      expect(regenerateFn).toHaveBeenCalledWith('Hair color mismatch');
      expect(mockAnalyzeImage).toHaveBeenCalledTimes(2);
    });

    it('should mark for manual review after max attempts', async () => {
      const failReport = makeReport({ passed: false, totalScore: 4.0, feedback: 'Poor quality' });
      mockAnalyzeImage.mockResolvedValue(failReport);

      const regenerateFn = vi.fn().mockResolvedValue('http://img.test/retry.jpg');
      const controller = new QualityController(mockVlm);
      const result = await controller.checkWithRetry(
        'http://img.test/1.jpg', 'char-1', 'img-1', 'task-1', regenerateFn, 7.0, 3,
      );

      expect(result.passed).toBe(false);
      expect(result.feedback).toContain('[MANUAL_REVIEW_REQUIRED]');
      expect(regenerateFn).toHaveBeenCalledTimes(2); // 3 attempts = 2 regenerations
      expect(mockAnalyzeImage).toHaveBeenCalledTimes(3);
    });

    it('should respect custom maxAttempts', async () => {
      const failReport = makeReport({ passed: false, totalScore: 3.0, feedback: 'Bad' });
      mockAnalyzeImage.mockResolvedValue(failReport);

      const regenerateFn = vi.fn().mockResolvedValue('http://img.test/retry.jpg');
      const controller = new QualityController(mockVlm);
      const result = await controller.checkWithRetry(
        'http://img.test/1.jpg', 'char-1', 'img-1', 'task-1', regenerateFn, 7.0, 2,
      );

      expect(result.passed).toBe(false);
      expect(result.feedback).toContain('[MANUAL_REVIEW_REQUIRED]');
      expect(regenerateFn).toHaveBeenCalledTimes(1); // 2 attempts = 1 regeneration
      expect(mockAnalyzeImage).toHaveBeenCalledTimes(2);
    });

    it('should pass new URL from regenerateFn to subsequent checks', async () => {
      const fail1 = makeReport({ passed: false, totalScore: 5.0, feedback: 'Fix hair' });
      const fail2 = makeReport({ passed: false, totalScore: 6.0, feedback: 'Fix skin' });
      const pass = makeReport({ passed: true, totalScore: 8.0 });
      mockAnalyzeImage
        .mockResolvedValueOnce(fail1)
        .mockResolvedValueOnce(fail2)
        .mockResolvedValueOnce(pass);

      const regenerateFn = vi.fn()
        .mockResolvedValueOnce('http://img.test/v2.jpg')
        .mockResolvedValueOnce('http://img.test/v3.jpg');

      const controller = new QualityController(mockVlm);
      const result = await controller.checkWithRetry(
        'http://img.test/v1.jpg', 'char-1', 'img-1', 'task-1', regenerateFn,
      );

      expect(result.passed).toBe(true);
      // First call uses original URL
      expect(mockAnalyzeImage.mock.calls[0][0]).toBe('http://img.test/v1.jpg');
      // Second call uses regenerated URL
      expect(mockAnalyzeImage.mock.calls[1][0]).toBe('http://img.test/v2.jpg');
      // Third call uses second regenerated URL
      expect(mockAnalyzeImage.mock.calls[2][0]).toBe('http://img.test/v3.jpg');
    });
  });
});
