/**
 * Conversation End-to-End WebSocket Tests
 *
 * Tests the full conversation flow through the WebSocket server.
 * These tests require the server to be running.
 *
 * Note: AI responses are non-deterministic, so these tests focus on:
 * - Response structure validation
 * - Message flow
 * - State changes
 */

import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import WebSocket from 'ws';

const WS_URL = 'ws://localhost:8001';
const TIMEOUT = 10000;
const AI_TIMEOUT = 30000;
const SETUP_TIMEOUT = 15000;

// Test user ID - use timestamp for isolation between test runs
const TEST_USER_ID = `test-e2e-convo-${Date.now()}`;

/**
 * Create WebSocket connection
 */
function createConnection(): Promise<WebSocket> {
  return new Promise((resolve, reject) => {
    const ws = new WebSocket(WS_URL);
    const timeout = setTimeout(() => {
      ws.close();
      reject(new Error('Connection timeout'));
    }, TIMEOUT);

    ws.on('open', () => {
      clearTimeout(timeout);
      resolve(ws);
    });

    ws.on('error', (err) => {
      clearTimeout(timeout);
      reject(err);
    });
  });
}

/**
 * Wait for a specific message type
 */
function waitForMessageType(
  ws: WebSocket,
  type: string,
  timeout = TIMEOUT
): Promise<any> {
  return new Promise((resolve, reject) => {
    const timeoutId = setTimeout(() => {
      ws.removeListener('message', handler);
      reject(new Error(`Timeout waiting for message type: ${type}`));
    }, timeout);

    const handler = (data: WebSocket.Data) => {
      try {
        const msg = JSON.parse(data.toString());
        if (msg.type === type) {
          clearTimeout(timeoutId);
          ws.removeListener('message', handler);
          resolve(msg);
        }
      } catch {
        // Ignore parse errors, continue waiting
      }
    };

    ws.on('message', handler);
  });
}

/**
 * Wait for any message, with timeout
 */
function waitForAnyMessage(
  ws: WebSocket,
  timeout = TIMEOUT
): Promise<any> {
  return new Promise((resolve, reject) => {
    const timeoutId = setTimeout(() => {
      ws.removeListener('message', handler);
      reject(new Error('Timeout waiting for any message'));
    }, timeout);

    const handler = (data: WebSocket.Data) => {
      clearTimeout(timeoutId);
      ws.removeListener('message', handler);
      try {
        resolve(JSON.parse(data.toString()));
      } catch {
        resolve({ raw: data.toString() });
      }
    };

    ws.once('message', handler);
  });
}

/**
 * Drain all pending messages for a short time
 */
async function drainMessages(ws: WebSocket, durationMs = 500): Promise<any[]> {
  const messages: any[] = [];
  return new Promise((resolve) => {
    const handler = (data: WebSocket.Data) => {
      try {
        messages.push(JSON.parse(data.toString()));
      } catch {
        messages.push({ raw: data.toString() });
      }
    };

    ws.on('message', handler);

    setTimeout(() => {
      ws.removeListener('message', handler);
      resolve(messages);
    }, durationMs);
  });
}

/**
 * Setup helper: Initialize player and create character with family
 */
async function setupTestPlayer(ws: WebSocket): Promise<{
  playerObj: any;
  characterId: string | null;
}> {
  // Send init message
  ws.send(JSON.stringify({
    type: 'init',
    userID: TEST_USER_ID,
  }));

  // Collect messages for a bit - server may send multiple
  let messages = await drainMessages(ws, 2000);

  // Find the playerObject message
  let playerObj = messages.find((m: any) => m.type === 'playerObject');

  if (!playerObj) {
    // Try waiting a bit more specifically for playerObject
    playerObj = await waitForMessageType(ws, 'playerObject', TIMEOUT);
  }

  if (!playerObj) {
    throw new Error('No playerObject received from server');
  }

  // Check if player needs character setup (status is 'creating' or no relationships)
  if (playerObj.status === 'creating' || !playerObj.r || playerObj.r.length === 0) {
    // Setup character - this creates family members as relationships
    ws.send(JSON.stringify({
      type: 'characterSetup',
      message: {
        name: 'TestPlayer',
        age: 18,
        sex: 'male',
      },
    }));

    // Collect messages from setup
    messages = await drainMessages(ws, 3000);

    // Find characterSetupComplete or updated playerObject
    const setupComplete = messages.find((m: any) => m.type === 'characterSetupComplete');
    const updatedPlayerObj = messages.find((m: any) => m.type === 'playerObject');

    if (!setupComplete && !updatedPlayerObj) {
      // Wait for them explicitly
      await waitForMessageType(ws, 'characterSetupComplete', SETUP_TIMEOUT);
    }

    // Get the final player object
    const finalPlayerObj = updatedPlayerObj || await waitForMessageType(ws, 'playerObject', TIMEOUT);

    const relationships = finalPlayerObj.r || [];
    const characterId = relationships.length > 0 ? relationships[0].id : null;

    return { playerObj: finalPlayerObj, characterId };
  }

  // Player already exists with relationships
  const relationships = playerObj.r || [];
  const characterId = relationships.length > 0 ? relationships[0].id : null;

  return { playerObj, characterId };
}

describe('Conversation E2E', () => {
  let ws: WebSocket | null = null;
  let serverAvailable = true;
  let characterId: string | null = null;
  let playerObj: any = null;

  beforeAll(async () => {
    try {
      // Connect to server
      ws = await createConnection();

      // Setup test player with character and relationships
      const setup = await setupTestPlayer(ws);
      playerObj = setup.playerObj;
      characterId = setup.characterId;

      if (!characterId) {
        throw new Error('Failed to setup test player with relationships');
      }
    } catch {
      serverAvailable = false;
      console.log('Server not available, skipping conversation E2E tests');
    }
  }, SETUP_TIMEOUT + TIMEOUT * 2);

  afterAll(() => {
    if (ws?.readyState === WebSocket.OPEN) {
      ws.close();
    }
  });

  describe('Connection and Initialization', () => {
    it('should have established WebSocket connection', () => {
      if (!serverAvailable || !ws) return;

      expect(ws.readyState).toBe(WebSocket.OPEN);
    });

    it('should have received player object with relationships', () => {
      if (!serverAvailable) return;

      expect(playerObj).toBeDefined();
      expect(playerObj.r).toBeDefined();
      expect(playerObj.r.length).toBeGreaterThan(0);
    });

    it('should have a valid character ID for testing', () => {
      if (!serverAvailable) return;

      expect(characterId).toBeDefined();
      expect(characterId).not.toBeNull();
      expect(typeof characterId).toBe('string');
    });
  });

  describe('Conversation Loading', () => {
    it('should load conversation for a character', async () => {
      if (!serverAvailable || !ws) return;

      // Send conversation load request (no response message = load only)
      ws.send(JSON.stringify({
        type: 'conversation',
        message: {
          conversationEvent: 'freeResponse',
          characterID: characterId,
          cType: 'chat',
        },
      }));

      // Should receive conversationEvent
      const convoResponse = await waitForMessageType(ws, 'conversationEvent', TIMEOUT);

      expect(convoResponse).toBeDefined();
      expect(convoResponse.type).toBe('conversationEvent');
      expect(convoResponse.id).toBeDefined();
      expect(Array.isArray(convoResponse.conversation)).toBe(true);
    });

    it('should have correct conversation structure', async () => {
      if (!serverAvailable || !ws) return;

      ws.send(JSON.stringify({
        type: 'conversation',
        message: {
          conversationEvent: 'freeResponse',
          characterID: characterId,
          cType: 'chat',
        },
      }));

      const convoResponse = await waitForMessageType(ws, 'conversationEvent', TIMEOUT);

      // Verify required fields
      expect(convoResponse).toHaveProperty('id');
      expect(convoResponse).toHaveProperty('type', 'conversationEvent');
      expect(convoResponse).toHaveProperty('conversation');
      expect(convoResponse).toHaveProperty('character');
    });
  });

  describe('Sending Messages', () => {
    it('should add player message to conversation', async () => {
      if (!serverAvailable || !ws) return;

      const tempId = `temp-${Date.now()}`;

      ws.send(JSON.stringify({
        type: 'conversation',
        message: {
          conversationEvent: 'freeResponse',
          characterID: characterId,
          cType: 'chat',
          response: 'Hello, how are you today?',
          tempId,
        },
      }));

      // First response includes our message
      const immediateResponse = await waitForMessageType(ws, 'conversationEvent', TIMEOUT);

      expect(immediateResponse.conversation).toBeDefined();
      expect(immediateResponse.conversation.length).toBeGreaterThan(0);

      // Find our message
      const playerMessage = immediateResponse.conversation.find(
        (m: any) => m.message === 'Hello, how are you today?'
      );
      expect(playerMessage).toBeDefined();
    });

    it('should receive AI response after sending message', async () => {
      if (!serverAvailable || !ws) return;

      const tempId = `temp-ai-${Date.now()}`;

      ws.send(JSON.stringify({
        type: 'conversation',
        message: {
          conversationEvent: 'freeResponse',
          characterID: characterId,
          cType: 'chat',
          response: 'What have you been up to lately?',
          tempId,
        },
      }));

      // Get immediate response with our message
      await waitForMessageType(ws, 'conversationEvent', TIMEOUT);

      // Wait for AI response (this takes longer)
      const aiResponse = await waitForMessageType(ws, 'conversationEvent', AI_TIMEOUT);

      expect(aiResponse.conversation).toBeDefined();
      expect(aiResponse.conversation.length).toBeGreaterThan(1);

      // Get last message (should be from AI/character)
      const lastMessage = aiResponse.conversation[aiResponse.conversation.length - 1];
      expect(lastMessage).toHaveProperty('id');
      expect(lastMessage).toHaveProperty('message');
      expect(lastMessage).toHaveProperty('datetime');

      // Message should have actual content
      expect(lastMessage.message.length).toBeGreaterThan(0);
    }, AI_TIMEOUT + 5000);

    it('should include sentiment in AI response', async () => {
      if (!serverAvailable || !ws) return;

      ws.send(JSON.stringify({
        type: 'conversation',
        message: {
          conversationEvent: 'freeResponse',
          characterID: characterId,
          cType: 'chat',
          response: 'That sounds really great!',
          tempId: `temp-sentiment-${Date.now()}`,
        },
      }));

      await waitForMessageType(ws, 'conversationEvent', TIMEOUT);
      const aiResponse = await waitForMessageType(ws, 'conversationEvent', AI_TIMEOUT);

      const lastMessage = aiResponse.conversation[aiResponse.conversation.length - 1];

      // Sentiment should be one of the valid values
      if (lastMessage.sentiment) {
        expect(['positive', 'negative', 'neutral']).toContain(lastMessage.sentiment);
      }
    }, AI_TIMEOUT + 5000);
  });

  describe('Person Object Retrieval', () => {
    it('should return person object when requested', async () => {
      if (!serverAvailable || !ws) return;

      ws.send(JSON.stringify({
        type: 'retrievePerson',
        message: characterId,
      }));

      const personResponse = await waitForMessageType(ws, 'personObject', TIMEOUT);

      expect(personResponse).toBeDefined();
      expect(personResponse.type).toBe('personObject');
      expect(personResponse.id).toBe(characterId);
      expect(personResponse).toHaveProperty('firstname');
    });
  });

  describe('Mark Read', () => {
    it('should mark conversation as read', async () => {
      if (!serverAvailable || !ws) return;

      // Load conversation first to get ID
      ws.send(JSON.stringify({
        type: 'conversation',
        message: {
          conversationEvent: 'freeResponse',
          characterID: characterId,
          cType: 'chat',
        },
      }));

      const convoResponse = await waitForMessageType(ws, 'conversationEvent', TIMEOUT);
      const conversationId = convoResponse.id;

      expect(conversationId).toBeDefined();

      // Mark as read
      ws.send(JSON.stringify({
        type: 'markRead',
        message: {
          conversationId,
        },
      }));

      // Wait for response - could be conversationMarkedRead or error
      const response = await Promise.race([
        waitForMessageType(ws, 'conversationMarkedRead', TIMEOUT),
        waitForMessageType(ws, 'error', TIMEOUT),
      ]);

      // If we got an error, the conversation might not exist in DB yet
      // This is acceptable for a new test user
      if (response.type === 'error') {
        expect(response.message).toBeDefined();
      } else {
        expect(response.success).toBe(true);
        expect(response.conversationId).toBe(conversationId);
      }
    }, TIMEOUT + 2000);
  });

  describe('Error Handling', () => {
    it('should return error for non-existent character', async () => {
      if (!serverAvailable || !ws) return;

      ws.send(JSON.stringify({
        type: 'conversation',
        message: {
          conversationEvent: 'freeResponse',
          characterID: 'non-existent-character-12345',
          cType: 'chat',
          response: 'Hello',
        },
      }));

      const errorResponse = await waitForMessageType(ws, 'error', TIMEOUT);

      expect(errorResponse).toBeDefined();
      expect(errorResponse.type).toBe('error');
      expect(errorResponse.message).toBeDefined();
    });
  });

  describe('Message Deduplication', () => {
    it('should not duplicate messages on sequential sends', async () => {
      if (!serverAvailable || !ws) return;

      // Send first message
      ws.send(JSON.stringify({
        type: 'conversation',
        message: {
          conversationEvent: 'freeResponse',
          characterID: characterId,
          cType: 'chat',
          response: 'First test message for dedup',
          tempId: `temp-dedup-1-${Date.now()}`,
        },
      }));

      await waitForMessageType(ws, 'conversationEvent', TIMEOUT);
      const firstAiResponse = await waitForMessageType(ws, 'conversationEvent', AI_TIMEOUT);
      const firstMessageCount = firstAiResponse.conversation.length;

      // Send second message
      ws.send(JSON.stringify({
        type: 'conversation',
        message: {
          conversationEvent: 'freeResponse',
          characterID: characterId,
          cType: 'chat',
          response: 'Second test message for dedup',
          tempId: `temp-dedup-2-${Date.now()}`,
        },
      }));

      await waitForMessageType(ws, 'conversationEvent', TIMEOUT);
      const secondAiResponse = await waitForMessageType(ws, 'conversationEvent', AI_TIMEOUT);

      // Verify message count increased (at least 1 new message - our message)
      // Note: AI may or may not respond, so we check for at least our message
      expect(secondAiResponse.conversation.length).toBeGreaterThanOrEqual(firstMessageCount + 1);

      // Verify no duplicate IDs - this is the main deduplication check
      const ids = secondAiResponse.conversation.map((m: any) => m.id);
      const uniqueIds = new Set(ids);
      expect(ids.length).toBe(uniqueIds.size);

      // Verify both our messages are in the conversation
      const ourMessages = secondAiResponse.conversation.filter(
        (m: any) => m.message === 'First test message for dedup' || m.message === 'Second test message for dedup'
      );
      expect(ourMessages.length).toBe(2);
    }, AI_TIMEOUT * 2 + 5000);
  });
});
