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

// Mock data-store before importing clone module
vi.mock('../data-store/index.js', () => {
  return {
    dataStore: {
      getCharacter: vi.fn(),
      saveCharacter: vi.fn(),
      saveMasterPrompt: vi.fn(),
      getMasterPrompt: vi.fn(),
      saveConversationProfile: vi.fn(),
      getConversationProfile: vi.fn(),
    },
  };
});

// Mock uuid to control generated IDs in tests
vi.mock('uuid', () => ({
  v4: vi.fn(() => 'new-clone-uuid-1234'),
}));

import { cloneCharacter } from './clone.js';
import { dataStore } from '../data-store/index.js';

function makeDNA(overrides?: Partial<CharacterDNA>): CharacterDNA {
  return {
    id: 'source-uuid-1234',
    createdAt: '2025-01-01T00:00:00Z',
    updatedAt: '2025-01-01T00:00:00Z',
    status: 'active',
    identity: { name: 'Aylin', age: 24, nationality: 'TR', city: 'Istanbul', occupation: 'Model', personality_tags: ['fun', 'outgoing'], mbti: 'ENFP', backstory: 'A vibrant soul' },
    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: ['mole on left cheek'] },
    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: 'left-45', signature_pose: 'hand-on-hip' },
    expressionMotion: { default_expression: 'smile', smile_type: 'warm', gaze_style: 'direct' },
    makeupStyle: { philosophy: 'natural', skin_finish: 'dewy', colors: { primary: '#C48793', secondary: '#E8B4B8', accent: '#8B0000' } },
    fashionStyle: { aesthetic: 'casual-chic', main_colors: ['#000000', '#FFFFFF', '#FF5733'], signature_pieces: ['jeans', 'blazer'] },
    technicalSettings: { camera_body: 'Canon R5', portrait_lens: '85mm f/1.4', full_body_lens: '35mm f/1.4', film_stock: 'Portra 400' },
    ...overrides,
  };
}

function makeProfile(characterId: string): ConversationProfile {
  return {
    characterId,
    style: 'neseli',
    toneOfVoice: {
      formality: 'casual',
      emoji_frequency: 'high',
      message_length: 'short',
      humor_level: 'frequent',
      flirtation_level: 'subtle',
    },
    languagePatterns: {
      common_phrases: ['haha', 'ya'],
      filler_words: ['şey', 'yani'],
      reaction_templates: { happy: ['yay!', 'süper!'] },
    },
    salesBehavior: {
      min_messages_before_pitch: 10,
      cooldown_minutes: 60,
      pitch_style: 'playful',
    },
  };
}

beforeEach(() => {
  vi.restoreAllMocks();
  vi.mocked(dataStore.getCharacter).mockResolvedValue(null);
  vi.mocked(dataStore.saveCharacter).mockResolvedValue(undefined);
  vi.mocked(dataStore.saveMasterPrompt).mockResolvedValue(undefined);
  vi.mocked(dataStore.saveConversationProfile).mockResolvedValue(undefined);
  vi.mocked(dataStore.getConversationProfile).mockResolvedValue(null);
});

describe('cloneCharacter', () => {
  it('should throw when source character is not found', async () => {
    await expect(cloneCharacter('nonexistent-id')).rejects.toThrow('Source character not found: nonexistent-id');
  });

  it('should assign a new UUID v4 to the cloned character', async () => {
    vi.mocked(dataStore.getCharacter).mockResolvedValue(makeDNA());

    const result = await cloneCharacter('source-uuid-1234');

    expect(result.id).toBe('new-clone-uuid-1234');
    expect(result.id).not.toBe('source-uuid-1234');
  });

  it('should set new createdAt and updatedAt timestamps', async () => {
    vi.mocked(dataStore.getCharacter).mockResolvedValue(makeDNA());

    const result = await cloneCharacter('source-uuid-1234');

    expect(result.createdAt).not.toBe('2025-01-01T00:00:00Z');
    // Both should be set from the same Date call
    expect(result.updatedAt).toBe(result.createdAt);
    // Should be a valid ISO timestamp
    expect(new Date(result.createdAt).toISOString()).toBe(result.createdAt);
  });

  it('should set status to creating', async () => {
    vi.mocked(dataStore.getCharacter).mockResolvedValue(makeDNA());

    const result = await cloneCharacter('source-uuid-1234');

    expect(result.status).toBe('creating');
  });

  it('should deep clone all DNA fields from source', async () => {
    vi.mocked(dataStore.getCharacter).mockResolvedValue(makeDNA());

    const result = await cloneCharacter('source-uuid-1234');

    expect(result.identity.name).toBe('Aylin');
    expect(result.skinMap.base).toBe('#F5D0A9');
    expect(result.hairDNA.root_color).toBe('#3B2F2F');
    expect(result.fashionStyle.main_colors).toEqual(['#000000', '#FFFFFF', '#FF5733']);
    expect(result.technicalSettings.camera_body).toBe('Canon R5');
  });

  it('should not mutate the source DNA', async () => {
    const source = makeDNA();
    vi.mocked(dataStore.getCharacter).mockResolvedValue(source);

    await cloneCharacter('source-uuid-1234', { identity: { name: 'Zeynep' } as any });

    expect(source.identity.name).toBe('Aylin');
    expect(source.id).toBe('source-uuid-1234');
  });

  it('should apply selective field overrides', async () => {
    vi.mocked(dataStore.getCharacter).mockResolvedValue(makeDNA());

    const result = await cloneCharacter('source-uuid-1234', {
      identity: { name: 'Zeynep', age: 22 } as any,
      skinMap: { base: '#AABBCC' } as any,
    });

    // Overridden fields
    expect(result.identity.name).toBe('Zeynep');
    expect(result.identity.age).toBe(22);
    expect(result.skinMap.base).toBe('#AABBCC');

    // Non-overridden fields preserved from merge
    expect(result.identity.city).toBe('Istanbul');
    expect(result.skinMap.shadow).toBe('#D4A574');
  });

  it('should always set status to creating regardless of overrides', async () => {
    vi.mocked(dataStore.getCharacter).mockResolvedValue(makeDNA());

    const result = await cloneCharacter('source-uuid-1234', { status: 'active' });

    // Status is always 'creating' for new clones (managed field)
    expect(result.status).toBe('creating');
  });

  it('should save the cloned character to data store', async () => {
    vi.mocked(dataStore.getCharacter).mockResolvedValue(makeDNA());

    await cloneCharacter('source-uuid-1234');

    expect(dataStore.saveCharacter).toHaveBeenCalledTimes(1);
    const savedDNA = vi.mocked(dataStore.saveCharacter).mock.calls[0][0];
    expect(savedDNA.id).toBe('new-clone-uuid-1234');
  });

  it('should rebuild and save master prompt from new DNA', async () => {
    vi.mocked(dataStore.getCharacter).mockResolvedValue(makeDNA());

    await cloneCharacter('source-uuid-1234');

    expect(dataStore.saveMasterPrompt).toHaveBeenCalledTimes(1);
    const savedPrompt = vi.mocked(dataStore.saveMasterPrompt).mock.calls[0][0] as MasterJSONPrompt;
    expect(savedPrompt.characterId).toBe('new-clone-uuid-1234');
    expect(savedPrompt.subject).toBeTruthy();
    expect(savedPrompt.camera).toContain('Canon R5');
  });

  it('should copy conversation profile with new characterId', async () => {
    vi.mocked(dataStore.getCharacter).mockResolvedValue(makeDNA());
    vi.mocked(dataStore.getConversationProfile).mockResolvedValue(makeProfile('source-uuid-1234'));

    await cloneCharacter('source-uuid-1234');

    expect(dataStore.saveConversationProfile).toHaveBeenCalledTimes(1);
    const savedProfile = vi.mocked(dataStore.saveConversationProfile).mock.calls[0][0] as ConversationProfile;
    expect(savedProfile.characterId).toBe('new-clone-uuid-1234');
    expect(savedProfile.style).toBe('neseli');
    expect(savedProfile.toneOfVoice.emoji_frequency).toBe('high');
  });

  it('should not save conversation profile if source has none', async () => {
    vi.mocked(dataStore.getCharacter).mockResolvedValue(makeDNA());

    await cloneCharacter('source-uuid-1234');

    expect(dataStore.saveConversationProfile).not.toHaveBeenCalled();
  });

  it('should deep clone conversation profile (no shared references)', async () => {
    const profile = makeProfile('source-uuid-1234');
    vi.mocked(dataStore.getCharacter).mockResolvedValue(makeDNA());
    vi.mocked(dataStore.getConversationProfile).mockResolvedValue(profile);

    await cloneCharacter('source-uuid-1234');

    const savedProfile = vi.mocked(dataStore.saveConversationProfile).mock.calls[0][0] as ConversationProfile;
    savedProfile.languagePatterns.common_phrases.push('test');
    expect(profile.languagePatterns.common_phrases).toEqual(['haha', 'ya']);
  });

  it('should ignore id, createdAt, updatedAt in overrides', async () => {
    vi.mocked(dataStore.getCharacter).mockResolvedValue(makeDNA());

    const result = await cloneCharacter('source-uuid-1234', {
      id: 'should-be-ignored',
      createdAt: '1999-01-01T00:00:00Z',
      updatedAt: '1999-01-01T00:00:00Z',
    });

    expect(result.id).toBe('new-clone-uuid-1234');
    expect(result.createdAt).not.toBe('1999-01-01T00:00:00Z');
  });

  it('should accept options parameter without error', async () => {
    vi.mocked(dataStore.getCharacter).mockResolvedValue(makeDNA());

    const result = await cloneCharacter('source-uuid-1234', undefined, { regenerateImages: true });

    expect(result.id).toBe('new-clone-uuid-1234');
  });
});
