import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { CharacterDNA, ConversationProfile, MasterJSONPrompt } from '../../types/character.js';

// --- Mocks ---

const mockSaveCharacter = vi.fn();
const mockGetCharacter = vi.fn();
const mockListCharacters = vi.fn();
const mockDeleteCharacter = vi.fn();
const mockSaveMasterPrompt = vi.fn();
const mockSaveConversationProfile = vi.fn();

vi.mock('../data-store/index.js', () => ({
  dataStore: {
    saveCharacter: (...args: any[]) => mockSaveCharacter(...args),
    getCharacter: (...args: any[]) => mockGetCharacter(...args),
    listCharacters: (...args: any[]) => mockListCharacters(...args),
    deleteCharacter: (...args: any[]) => mockDeleteCharacter(...args),
    saveMasterPrompt: (...args: any[]) => mockSaveMasterPrompt(...args),
    saveConversationProfile: (...args: any[]) => mockSaveConversationProfile(...args),
  },
}));

const mockGenerateReferenceImages = vi.fn().mockResolvedValue(['task-1', 'task-2']);
vi.mock('./reference-pipeline.js', () => ({
  generateReferenceImages: (...args: any[]) => mockGenerateReferenceImages(...args),
}));

const mockCloneCharacter = vi.fn();
vi.mock('./clone.js', () => ({
  cloneCharacter: (...args: any[]) => mockCloneCharacter(...args),
}));

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

// Import after mocks
import { CharacterEngine } from './index.js';

// --- Helpers ---

function makeDNA(overrides?: Partial<CharacterDNA>): CharacterDNA {
  return {
    id: 'test-id-1',
    createdAt: '2025-01-01T00:00:00Z',
    updatedAt: '2025-01-01T00:00:00Z',
    status: 'active',
    identity: { name: 'Aylin', age: 24, nationality: 'Turkish', city: 'Istanbul', occupation: 'Model', personality_tags: ['fun'], mbti: 'ENFP', backstory: 'A model' },
    physicalMeasurements: { height_cm: 170, weight_kg: 55, body_type: 'slim', measurements: { bust: 85, waist: 60, hips: 90 } },
    skinMap: { base: '#F5D0A9', shadow: '#D4A574', highlight: '#FDE8D0' },
    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: '#1A1A1A', mid_color: '#2C2C2C', highlight_color: '#4A3728' },
    bodyBiometrics: { body_type_detail: 'athletic slim', posture: 'upright', skin_texture: 'smooth' },
    profileView: { best_angle: 'three-quarter left', signature_pose: 'hand on hip' },
    expressionMotion: { default_expression: 'soft smile', smile_type: 'closed lip', gaze_style: 'direct' },
    makeupStyle: { philosophy: 'natural glam', skin_finish: 'dewy', colors: { primary: '#C48A6A', secondary: '#8B5E3C', accent: '#D4756B' } },
    fashionStyle: { aesthetic: 'casual chic', main_colors: ['#000000', '#FFFFFF', '#C48A6A'], signature_pieces: ['jeans', 'blazer'] },
    technicalSettings: { camera_body: 'Sony A7IV', portrait_lens: '85mm f/1.4', full_body_lens: '35mm f/1.4', film_stock: 'Kodak Portra 400' },
    ...overrides,
  };
}

// --- Tests ---

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

  beforeEach(() => {
    vi.clearAllMocks();
    engine = new CharacterEngine();
  });

  describe('createCharacter', () => {
    it('should create a character with full pipeline', async () => {
      mockSaveCharacter.mockResolvedValue(undefined);
      mockSaveMasterPrompt.mockResolvedValue(undefined);
      mockGenerateReferenceImages.mockResolvedValue(['t1']);

      const result = await engine.createCharacter({ identity: { name: 'Selin', age: 22, nationality: 'Turkish', city: 'Ankara', occupation: 'Student', personality_tags: [], mbti: 'INFJ', backstory: '' } });

      expect(result.identity.name).toBe('Selin');
      expect(result.status).toBe('active');
      // saveCharacter called twice: once with 'creating', once with 'active'
      expect(mockSaveCharacter).toHaveBeenCalledTimes(2);
      expect(mockSaveMasterPrompt).toHaveBeenCalledTimes(1);
      expect(mockGenerateReferenceImages).toHaveBeenCalledTimes(1);
    });

    it('should save conversation profile when provided', async () => {
      mockSaveCharacter.mockResolvedValue(undefined);
      mockSaveMasterPrompt.mockResolvedValue(undefined);
      mockGenerateReferenceImages.mockResolvedValue([]);
      mockSaveConversationProfile.mockResolvedValue(undefined);

      await engine.createCharacter({}, { style: 'tripli' });

      expect(mockSaveConversationProfile).toHaveBeenCalledTimes(1);
      const savedProfile = mockSaveConversationProfile.mock.calls[0][0];
      expect(savedProfile.style).toBe('tripli');
    });

    it('should fill defaults for missing fields', async () => {
      mockSaveCharacter.mockResolvedValue(undefined);
      mockSaveMasterPrompt.mockResolvedValue(undefined);
      mockGenerateReferenceImages.mockResolvedValue([]);

      const result = await engine.createCharacter({});

      expect(result.identity.name).toBe('Unnamed Character');
      expect(result.skinMap.base).toBe('#F5D0A9');
      expect(result.id).toBeTruthy();
    });

    it('should set initial status to creating then active', async () => {
      const savedStatuses: string[] = [];
      mockSaveCharacter.mockImplementation(async (dna: CharacterDNA) => {
        savedStatuses.push(dna.status);
      });
      mockSaveMasterPrompt.mockResolvedValue(undefined);
      mockGenerateReferenceImages.mockResolvedValue([]);

      await engine.createCharacter({});

      expect(savedStatuses[0]).toBe('creating');
      expect(savedStatuses[1]).toBe('active');
    });
  });

  describe('updateCharacter', () => {
    it('should merge updates into existing character', async () => {
      const existing = makeDNA();
      mockGetCharacter.mockResolvedValue(existing);
      mockSaveCharacter.mockResolvedValue(undefined);

      const result = await engine.updateCharacter('test-id-1', {
        identity: { name: 'Updated Name', age: 24, nationality: 'Turkish', city: 'Istanbul', occupation: 'Model', personality_tags: ['fun'], mbti: 'ENFP', backstory: 'A model' },
      });

      expect(result.identity.name).toBe('Updated Name');
      expect(mockSaveCharacter).toHaveBeenCalledTimes(1);
    });

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

      await expect(engine.updateCharacter('nonexistent', {})).rejects.toThrow('Character not found');
    });

    it('should update the updatedAt timestamp', async () => {
      const existing = makeDNA({ updatedAt: '2020-01-01T00:00:00Z' });
      mockGetCharacter.mockResolvedValue(existing);
      mockSaveCharacter.mockResolvedValue(undefined);

      const result = await engine.updateCharacter('test-id-1', {
        skinMap: { base: '#AAAAAA', shadow: '#BBBBBB', highlight: '#CCCCCC' },
      });

      expect(result.updatedAt).not.toBe('2020-01-01T00:00:00Z');
    });

    it('should deep merge nested objects like measurements', async () => {
      const existing = makeDNA();
      mockGetCharacter.mockResolvedValue(existing);
      mockSaveCharacter.mockResolvedValue(undefined);

      const result = await engine.updateCharacter('test-id-1', {
        physicalMeasurements: { height_cm: 175, weight_kg: 55, body_type: 'slim', measurements: { bust: 90, waist: 60, hips: 90 } },
      });

      expect(result.physicalMeasurements.height_cm).toBe(175);
      expect(result.physicalMeasurements.measurements.bust).toBe(90);
    });
  });

  describe('deleteCharacter', () => {
    it('should return true when character exists', async () => {
      mockDeleteCharacter.mockResolvedValue(true);

      const result = await engine.deleteCharacter('test-id-1');
      expect(result).toBe(true);
      expect(mockDeleteCharacter).toHaveBeenCalledWith('test-id-1');
    });

    it('should return false when character does not exist', async () => {
      mockDeleteCharacter.mockResolvedValue(false);

      const result = await engine.deleteCharacter('nonexistent');
      expect(result).toBe(false);
    });
  });

  describe('getCharacter', () => {
    it('should return character when found', async () => {
      const dna = makeDNA();
      mockGetCharacter.mockResolvedValue(dna);

      const result = await engine.getCharacter('test-id-1');
      expect(result).toEqual(dna);
    });

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

      const result = await engine.getCharacter('nonexistent');
      expect(result).toBeNull();
    });
  });

  describe('listCharacters', () => {
    it('should return all characters', async () => {
      const chars = [makeDNA({ id: 'a' }), makeDNA({ id: 'b' })];
      mockListCharacters.mockResolvedValue(chars);

      const result = await engine.listCharacters();
      expect(result).toHaveLength(2);
    });

    it('should return empty array when no characters', async () => {
      mockListCharacters.mockResolvedValue([]);

      const result = await engine.listCharacters();
      expect(result).toEqual([]);
    });
  });

  describe('activateCharacter', () => {
    it('should set status to active', async () => {
      const dna = makeDNA({ status: 'inactive' });
      mockGetCharacter.mockResolvedValue(dna);
      mockSaveCharacter.mockResolvedValue(undefined);

      const result = await engine.activateCharacter('test-id-1');
      expect(result.status).toBe('active');
      expect(mockSaveCharacter).toHaveBeenCalledTimes(1);
    });

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

      await expect(engine.activateCharacter('nonexistent')).rejects.toThrow('Character not found');
    });
  });

  describe('deactivateCharacter', () => {
    it('should set status to inactive', async () => {
      const dna = makeDNA({ status: 'active' });
      mockGetCharacter.mockResolvedValue(dna);
      mockSaveCharacter.mockResolvedValue(undefined);

      const result = await engine.deactivateCharacter('test-id-1');
      expect(result.status).toBe('inactive');
    });

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

      await expect(engine.deactivateCharacter('nonexistent')).rejects.toThrow('Character not found');
    });
  });

  describe('cloneCharacter', () => {
    it('should delegate to clone module', async () => {
      const cloned = makeDNA({ id: 'cloned-id' });
      mockCloneCharacter.mockResolvedValue(cloned);

      const result = await engine.cloneCharacter('source-id', { identity: { name: 'Clone', age: 20, nationality: 'TR', city: 'Izmir', occupation: 'Artist', personality_tags: [], mbti: 'INTP', backstory: '' } });

      expect(mockCloneCharacter).toHaveBeenCalledWith('source-id', expect.objectContaining({ identity: expect.objectContaining({ name: 'Clone' }) }));
      expect(result.id).toBe('cloned-id');
    });
  });
});
