/**
 * DrizzleMessageRepository Tests
 *
 * Tests for the Message repository adapter.
 */

import { describe, it, expect, beforeEach } from 'vitest';
import { DrizzleMessageRepository } from './message.js';
import { DrizzleAgentRepository } from './agent.js';
import { DrizzleTaskRepository } from './task.js';
import { DrizzlePhaseRepository } from './phase.js';
import { DrizzleInitiativeRepository } from './initiative.js';
import { createTestDatabase } from './test-helpers.js';
import type { DrizzleDatabase } from '../../index.js';

describe('DrizzleMessageRepository', () => {
  let db: DrizzleDatabase;
  let messageRepo: DrizzleMessageRepository;
  let agentRepo: DrizzleAgentRepository;
  let testAgentId: string;

  beforeEach(async () => {
    db = createTestDatabase();
    messageRepo = new DrizzleMessageRepository(db);
    agentRepo = new DrizzleAgentRepository(db);

    // Create required hierarchy for agent FK
    const taskRepo = new DrizzleTaskRepository(db);
    const phaseRepo = new DrizzlePhaseRepository(db);
    const initiativeRepo = new DrizzleInitiativeRepository(db);

    const initiative = await initiativeRepo.create({
      name: 'Test Initiative',
    });
    const phase = await phaseRepo.create({
      initiativeId: initiative.id,
      name: 'Test Phase',
    });
    const task = await taskRepo.create({
      phaseId: phase.id,
      name: 'Test Task',
      order: 1,
    });

    // Create a test agent
    const agent = await agentRepo.create({
      name: 'test-agent',
      worktreeId: 'worktree-123',
      taskId: task.id,
    });
    testAgentId = agent.id;
  });

  describe('create', () => {
    it('should create agent→user message (question)', async () => {
      const message = await messageRepo.create({
        senderType: 'agent',
        senderId: testAgentId,
        recipientType: 'user',
        type: 'question',
        content: 'Do you want to proceed with deployment?',
        requiresResponse: true,
      });

      expect(message.id).toBeDefined();
      expect(message.id.length).toBeGreaterThan(0);
      expect(message.senderType).toBe('agent');
      expect(message.senderId).toBe(testAgentId);
      expect(message.recipientType).toBe('user');
      expect(message.recipientId).toBeNull();
      expect(message.type).toBe('question');
      expect(message.content).toBe('Do you want to proceed with deployment?');
      expect(message.requiresResponse).toBe(true);
      expect(message.status).toBe('pending');
      expect(message.parentMessageId).toBeNull();
      expect(message.createdAt).toBeInstanceOf(Date);
      expect(message.updatedAt).toBeInstanceOf(Date);
    });

    it('should create agent→user message (notification, requiresResponse=false)', async () => {
      const message = await messageRepo.create({
        senderType: 'agent',
        senderId: testAgentId,
        recipientType: 'user',
        type: 'info',
        content: 'Build completed successfully.',
        requiresResponse: false,
      });

      expect(message.type).toBe('info');
      expect(message.requiresResponse).toBe(false);
      expect(message.status).toBe('pending');
    });

    it('should create user→agent response', async () => {
      // First create the question
      const question = await messageRepo.create({
        senderType: 'agent',
        senderId: testAgentId,
        recipientType: 'user',
        type: 'question',
        content: 'Which database?',
        requiresResponse: true,
      });

      // Then create user response
      const response = await messageRepo.create({
        senderType: 'user',
        recipientType: 'agent',
        recipientId: testAgentId,
        type: 'response',
        content: 'Use PostgreSQL',
        parentMessageId: question.id,
      });

      expect(response.senderType).toBe('user');
      expect(response.senderId).toBeNull();
      expect(response.recipientType).toBe('agent');
      expect(response.recipientId).toBe(testAgentId);
      expect(response.parentMessageId).toBe(question.id);
    });

    it('should default type to info', async () => {
      const message = await messageRepo.create({
        senderType: 'agent',
        senderId: testAgentId,
        recipientType: 'user',
        content: 'Status update',
      });

      expect(message.type).toBe('info');
    });
  });

  describe('findById', () => {
    it('should return null for non-existent message', async () => {
      const result = await messageRepo.findById('non-existent-id');
      expect(result).toBeNull();
    });

    it('should find an existing message', async () => {
      const created = await messageRepo.create({
        senderType: 'agent',
        senderId: testAgentId,
        recipientType: 'user',
        content: 'Test message',
      });

      const found = await messageRepo.findById(created.id);
      expect(found).not.toBeNull();
      expect(found!.id).toBe(created.id);
      expect(found!.content).toBe('Test message');
    });
  });

  describe('findBySender', () => {
    it('should find messages by agent sender', async () => {
      await messageRepo.create({
        senderType: 'agent',
        senderId: testAgentId,
        recipientType: 'user',
        content: 'Message 1',
      });
      await messageRepo.create({
        senderType: 'agent',
        senderId: testAgentId,
        recipientType: 'user',
        content: 'Message 2',
      });

      const messages = await messageRepo.findBySender('agent', testAgentId);
      expect(messages.length).toBe(2);
    });

    it('should find messages by user sender', async () => {
      await messageRepo.create({
        senderType: 'user',
        recipientType: 'agent',
        recipientId: testAgentId,
        content: 'User message 1',
      });
      await messageRepo.create({
        senderType: 'user',
        recipientType: 'agent',
        recipientId: testAgentId,
        content: 'User message 2',
      });

      const messages = await messageRepo.findBySender('user');
      expect(messages.length).toBe(2);
    });

    it('should return empty array when no messages from sender', async () => {
      const messages = await messageRepo.findBySender('agent', 'nonexistent-id');
      expect(messages).toEqual([]);
    });
  });

  describe('findByRecipient', () => {
    it('should find messages by user recipient', async () => {
      await messageRepo.create({
        senderType: 'agent',
        senderId: testAgentId,
        recipientType: 'user',
        content: 'For user 1',
      });
      await messageRepo.create({
        senderType: 'agent',
        senderId: testAgentId,
        recipientType: 'user',
        content: 'For user 2',
      });

      const messages = await messageRepo.findByRecipient('user');
      expect(messages.length).toBe(2);
    });

    it('should find messages by agent recipient', async () => {
      await messageRepo.create({
        senderType: 'user',
        recipientType: 'agent',
        recipientId: testAgentId,
        content: 'For agent',
      });

      const messages = await messageRepo.findByRecipient('agent', testAgentId);
      expect(messages.length).toBe(1);
    });

    it('should return empty array when no messages for recipient', async () => {
      const messages = await messageRepo.findByRecipient('agent', 'nonexistent-id');
      expect(messages).toEqual([]);
    });
  });

  describe('findPendingForUser', () => {
    it('should return only user-targeted pending messages', async () => {
      // Create pending message for user
      await messageRepo.create({
        senderType: 'agent',
        senderId: testAgentId,
        recipientType: 'user',
        content: 'Pending for user',
        status: 'pending',
      });

      // Create read message for user (should not be returned)
      const readMessage = await messageRepo.create({
        senderType: 'agent',
        senderId: testAgentId,
        recipientType: 'user',
        content: 'Already read',
      });
      await messageRepo.update(readMessage.id, { status: 'read' });

      // Create pending message for agent (should not be returned)
      await messageRepo.create({
        senderType: 'user',
        recipientType: 'agent',
        recipientId: testAgentId,
        content: 'For agent not user',
      });

      const pending = await messageRepo.findPendingForUser();
      expect(pending.length).toBe(1);
      expect(pending[0].content).toBe('Pending for user');
    });

    it('should return empty array when no pending messages for user', async () => {
      const pending = await messageRepo.findPendingForUser();
      expect(pending).toEqual([]);
    });
  });

  describe('findRequiringResponse', () => {
    it('should return only messages needing response', async () => {
      // Create message requiring response
      await messageRepo.create({
        senderType: 'agent',
        senderId: testAgentId,
        recipientType: 'user',
        type: 'question',
        content: 'Requires answer',
        requiresResponse: true,
      });

      // Create message not requiring response
      await messageRepo.create({
        senderType: 'agent',
        senderId: testAgentId,
        recipientType: 'user',
        type: 'info',
        content: 'Just info',
        requiresResponse: false,
      });

      // Create message that required response but was responded
      const responded = await messageRepo.create({
        senderType: 'agent',
        senderId: testAgentId,
        recipientType: 'user',
        type: 'question',
        content: 'Already answered',
        requiresResponse: true,
      });
      await messageRepo.update(responded.id, { status: 'responded' });

      const requiring = await messageRepo.findRequiringResponse();
      expect(requiring.length).toBe(1);
      expect(requiring[0].content).toBe('Requires answer');
    });

    it('should return empty array when no messages require response', async () => {
      const requiring = await messageRepo.findRequiringResponse();
      expect(requiring).toEqual([]);
    });
  });

  describe('findReplies (message threading)', () => {
    it('should find all replies to a parent message', async () => {
      // Create original question
      const question = await messageRepo.create({
        senderType: 'agent',
        senderId: testAgentId,
        recipientType: 'user',
        type: 'question',
        content: 'Original question',
        requiresResponse: true,
      });

      // Create two replies
      await messageRepo.create({
        senderType: 'user',
        recipientType: 'agent',
        recipientId: testAgentId,
        type: 'response',
        content: 'First reply',
        parentMessageId: question.id,
      });
      await messageRepo.create({
        senderType: 'user',
        recipientType: 'agent',
        recipientId: testAgentId,
        type: 'response',
        content: 'Second reply',
        parentMessageId: question.id,
      });

      // Create unrelated message (should not be in replies)
      await messageRepo.create({
        senderType: 'agent',
        senderId: testAgentId,
        recipientType: 'user',
        content: 'Unrelated message',
      });

      const replies = await messageRepo.findReplies(question.id);
      expect(replies.length).toBe(2);
      expect(replies.every((r) => r.parentMessageId === question.id)).toBe(true);
    });

    it('should return empty array when message has no replies', async () => {
      const message = await messageRepo.create({
        senderType: 'agent',
        senderId: testAgentId,
        recipientType: 'user',
        content: 'No replies',
      });

      const replies = await messageRepo.findReplies(message.id);
      expect(replies).toEqual([]);
    });
  });

  describe('update status flow', () => {
    it('should update status: pending → read → responded', async () => {
      const message = await messageRepo.create({
        senderType: 'agent',
        senderId: testAgentId,
        recipientType: 'user',
        type: 'question',
        content: 'Status flow test',
        requiresResponse: true,
      });
      expect(message.status).toBe('pending');

      // Wait to ensure different timestamps
      await new Promise((resolve) => setTimeout(resolve, 10));

      // Update to read
      const readMessage = await messageRepo.update(message.id, { status: 'read' });
      expect(readMessage.status).toBe('read');
      expect(readMessage.updatedAt.getTime()).toBeGreaterThanOrEqual(message.updatedAt.getTime());

      // Wait again
      await new Promise((resolve) => setTimeout(resolve, 10));

      // Update to responded
      const respondedMessage = await messageRepo.update(readMessage.id, {
        status: 'responded',
      });
      expect(respondedMessage.status).toBe('responded');
      expect(respondedMessage.updatedAt.getTime()).toBeGreaterThanOrEqual(
        readMessage.updatedAt.getTime()
      );
    });
  });

  describe('update', () => {
    it('should throw for non-existent message', async () => {
      await expect(
        messageRepo.update('non-existent-id', { status: 'read' })
      ).rejects.toThrow('Message not found');
    });

    it('should update content and updatedAt', async () => {
      const created = await messageRepo.create({
        senderType: 'agent',
        senderId: testAgentId,
        recipientType: 'user',
        content: 'Original content',
      });

      await new Promise((resolve) => setTimeout(resolve, 10));

      const updated = await messageRepo.update(created.id, {
        content: 'Updated content',
      });

      expect(updated.content).toBe('Updated content');
      expect(updated.updatedAt.getTime()).toBeGreaterThanOrEqual(created.updatedAt.getTime());
    });
  });

  describe('delete', () => {
    it('should delete an existing message', async () => {
      const created = await messageRepo.create({
        senderType: 'agent',
        senderId: testAgentId,
        recipientType: 'user',
        content: 'To delete',
      });

      await messageRepo.delete(created.id);

      const found = await messageRepo.findById(created.id);
      expect(found).toBeNull();
    });

    it('should throw for non-existent message', async () => {
      await expect(messageRepo.delete('non-existent-id')).rejects.toThrow(
        'Message not found'
      );
    });
  });
});
