import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { DraftService } from './draft-service';
import { db } from './index';
import { SyncManager } from '../../services/sync-manager';

// Mock SyncManager to prevent side effects in service tests
vi.mock('../../services/sync-manager', () => ({
  SyncManager: {
    isOnline: vi.fn(),
    queueAction: vi.fn(),
  },
}));

describe('DraftService', () => {
  beforeEach(async () => {
    await db.delete();
    await db.open();

    // Default to online for most tests
    vi.mocked(SyncManager.isOnline).mockReturnValue(true);
    vi.mocked(SyncManager.queueAction).mockResolvedValue(1);
  });

  afterEach(async () => {
    await db.delete();
    vi.clearAllMocks();
  });

  describe('saveDraft', () => {
    it('should save a draft to IndexedDB', async () => {
      const draft = {
        sessionId: 'session-123',
        title: 'My Learning Moment',
        content: '# Today I learned about closures',
        tags: ['javascript', 'learning'],
        createdAt: Date.now(),
        status: 'draft' as const,
      };

      const savedDraft = await DraftService.saveDraft(draft);

      expect(savedDraft.id).toBeDefined();
      expect(savedDraft.sessionId).toBe(draft.sessionId);
      expect(savedDraft.title).toBe(draft.title);
      expect(savedDraft.content).toBe(draft.content);
      expect(savedDraft.tags).toEqual(draft.tags);
    });

    it('should queue action when offline', async () => {
      vi.mocked(SyncManager.isOnline).mockReturnValue(false);

      const draft = {
        sessionId: 'offline-session',
        title: 'Offline Draft',
        content: 'Content',
        tags: [],
        createdAt: Date.now(),
        status: 'draft' as const,
      };

      await DraftService.saveDraft(draft);

      // Should still save locally
      const saved = await db.drafts.where('sessionId').equals('offline-session').first();
      expect(saved).toBeDefined();

      // But ALSO queue the action
      expect(SyncManager.queueAction).toHaveBeenCalledWith('saveDraft', expect.objectContaining({
        draftData: expect.objectContaining({ title: 'Offline Draft' })
      }));
    });

    it('should return a plain object (not Dexie observable)', async () => {
      const draft = {
        sessionId: 'session-123',
        title: 'Test Draft',
        content: '# Test',
        tags: ['test'],
        createdAt: Date.now(),
        status: 'draft' as const,
      };

      const savedDraft = await DraftService.saveDraft(draft);

      // Should be a plain object, not a Dexie observable
      expect(savedDraft.constructor).toBe(Object);
    });
  });

  describe('getDraftBySessionId', () => {
    it('should retrieve draft by session ID', async () => {
      const draft = {
        sessionId: 'session-123',
        title: 'My Draft',
        content: '# Content',
        tags: ['tag1'],
        createdAt: Date.now(),
        status: 'draft' as const,
      };

      await DraftService.saveDraft(draft);

      const retrieved = await DraftService.getDraftBySessionId('session-123');

      expect(retrieved).toBeDefined();
      expect(retrieved?.sessionId).toBe('session-123');
      expect(retrieved?.title).toBe('My Draft');
    });

    it('should return null for non-existent session', async () => {
      const retrieved = await DraftService.getDraftBySessionId('non-existent');
      expect(retrieved).toBeNull();
    });

    it('should return the latest draft if multiple exist for a session', async () => {
      const timestamp = Date.now();

      await DraftService.saveDraft({
        sessionId: 'session-123',
        title: 'First Draft',
        content: '# First',
        tags: ['tag1'],
        createdAt: timestamp - 1000,
        status: 'regenerated' as const,
      });

      await DraftService.saveDraft({
        sessionId: 'session-123',
        title: 'Second Draft',
        content: '# Second',
        tags: ['tag2'],
        createdAt: timestamp,
        status: 'draft' as const,
      });

      const retrieved = await DraftService.getDraftBySessionId('session-123');

      expect(retrieved?.title).toBe('Second Draft');
      expect(retrieved?.status).toBe('draft');
    });
  });

  describe('getAllDrafts', () => {
    it('should return all drafts ordered by creation date', async () => {
      const timestamp = Date.now();

      await DraftService.saveDraft({
        sessionId: 'session-1',
        title: 'Draft 1',
        content: '# Content 1',
        tags: ['tag1'],
        createdAt: timestamp - 2000,
        status: 'completed' as const,
      });

      await DraftService.saveDraft({
        sessionId: 'session-2',
        title: 'Draft 2',
        content: '# Content 2',
        tags: ['tag2'],
        createdAt: timestamp - 1000,
        status: 'draft' as const,
      });

      await DraftService.saveDraft({
        sessionId: 'session-3',
        title: 'Draft 3',
        content: '# Content 3',
        tags: ['tag3'],
        createdAt: timestamp,
        status: 'draft' as const,
      });

      const allDrafts = await DraftService.getAllDrafts();

      expect(allDrafts).toHaveLength(3);
      expect(allDrafts[0].title).toBe('Draft 3'); // Most recent first
      expect(allDrafts[1].title).toBe('Draft 2');
      expect(allDrafts[2].title).toBe('Draft 1');
    });

    it('should return empty array when no drafts exist', async () => {
      const allDrafts = await DraftService.getAllDrafts();
      expect(allDrafts).toEqual([]);
    });
  });

  describe('updateDraftStatus', () => {
    it('should update draft status', async () => {
      const draft = await DraftService.saveDraft({
        sessionId: 'session-123',
        title: 'My Draft',
        content: '# Content',
        tags: ['tag1'],
        createdAt: Date.now(),
        status: 'draft' as const,
      });

      if (!draft.id) throw new Error('Draft ID should be defined');

      await DraftService.updateDraftStatus(draft.id, 'completed');

      const updated = await DraftService.getDraftBySessionId('session-123');
      expect(updated?.status).toBe('completed');
    });

    it('should handle non-existent draft ID gracefully', async () => {
      const result = await DraftService.updateDraftStatus(99999, 'completed');
      expect(result).toBe(false);
    });
  });

  describe('deleteDraft', () => {
    it('should delete a draft', async () => {
      const draft = await DraftService.saveDraft({
        sessionId: 'session-123',
        title: 'To Delete',
        content: '# Content',
        tags: ['tag1'],
        createdAt: Date.now(),
        status: 'draft' as const,
      });

      if (!draft.id) throw new Error('Draft ID should be defined');

      const deleted = await DraftService.deleteDraft(draft.id);
      expect(deleted).toBe(true);

      const retrieved = await DraftService.getDraftBySessionId('session-123');
      expect(retrieved).toBeNull();
    });

    it('should return false for non-existent draft', async () => {
      const deleted = await DraftService.deleteDraft(99999);
      expect(deleted).toBe(false);
    });

    // Story 3.2: Cascade delete tests
    it('should delete associated chat logs when deleting draft (cascade delete)', async () => {
      const sessionId = 'session-123';

      // Create chat logs for the session with sessionId
      await db.chatLogs.bulkAdd([
        { sessionId, role: 'user' as const, content: 'My message', timestamp: Date.now() },
        { sessionId, role: 'assistant' as const, content: 'AI response', timestamp: Date.now() },
      ]);

      // Create draft
      const draft = await DraftService.saveDraft({
        sessionId,
        title: 'To Delete',
        content: '# Content',
        tags: ['tag1'],
        createdAt: Date.now(),
        status: 'draft' as const,
      });

      if (!draft.id) throw new Error('Draft ID should be defined');

      // Verify chat logs exist
      const chatLogsBefore = await db.chatLogs.where('sessionId').equals(sessionId).toArray();
      expect(chatLogsBefore.length).toBe(2);

      // Delete draft (should also delete chat logs)
      await DraftService.deleteDraft(draft.id);

      // Verify draft is deleted
      const retrieved = await DraftService.getDraftById(draft.id);
      expect(retrieved).toBeNull();

      // Verify chat logs are also deleted (cascade)
      const chatLogsAfter = await db.chatLogs.where('sessionId').equals(sessionId).toArray();
      expect(chatLogsAfter.length).toBe(0);
    });

    it('should use transaction for atomic cascade delete', async () => {
      const sessionId = 'session-123';

      // Create chat logs with sessionId
      await db.chatLogs.add({
        sessionId,
        role: 'user' as const,
        content: 'Test message',
        timestamp: Date.now(),
      });

      // Create draft
      const draft = await DraftService.saveDraft({
        sessionId,
        title: 'Test Draft',
        content: '# Test',
        tags: [],
        createdAt: Date.now(),
        status: 'draft' as const,
      });

      if (!draft.id) throw new Error('Draft ID should be defined');

      // Delete should be atomic - both draft and chat logs deleted together
      const result = await DraftService.deleteDraft(draft.id);
      expect(result).toBe(true);

      // Both should be gone (atomic operation)
      const draftExists = await db.drafts.get(draft.id);
      const chatLogsExist = await db.chatLogs.where('sessionId').equals(sessionId).toArray();

      expect(draftExists).toBeUndefined();
      expect(chatLogsExist.length).toBe(0);
    });

    it('should handle non-existent draft gracefully (idempotent)', async () => {
      const sessionId = 'session-123';

      // Create chat logs for the session with sessionId
      await db.chatLogs.add({
        sessionId,
        role: 'user' as const,
        content: 'My message',
        timestamp: Date.now(),
      });

      // Try to delete non-existent draft
      const result = await DraftService.deleteDraft(99999);

      // Should return false but not throw
      expect(result).toBe(false);

      // Chat logs should still exist (draft didn't exist so nothing to cascade)
      const chatLogs = await db.chatLogs.toArray();
      expect(chatLogs.length).toBe(1);
    });
  });

  describe('markAsCompleted', () => {
    it('should mark a draft as completed with timestamp', async () => {
      const draft = await DraftService.saveDraft({
        sessionId: 'session-123',
        title: 'My Learning Moment',
        content: '# Today I learned',
        tags: ['growth'],
        createdAt: Date.now(),
        status: 'draft' as const,
      });

      if (!draft.id) throw new Error('Draft ID should be defined');

      const beforeCompletion = Date.now();
      const completed = await DraftService.markAsCompleted(draft.id);

      expect(completed).not.toBeNull();
      expect(completed?.status).toBe('completed');
      expect(completed?.completedAt).toBeDefined();
      expect(completed?.completedAt).toBeGreaterThanOrEqual(beforeCompletion);
    });

    it('should return null for non-existent draft', async () => {
      const completed = await DraftService.markAsCompleted(99999);
      expect(completed).toBeNull();
    });

    it('should return the same draft if already completed', async () => {
      const draft = await DraftService.saveDraft({
        sessionId: 'session-123',
        title: 'Already Completed',
        content: '# Content',
        tags: [],
        createdAt: Date.now(),
        status: 'completed' as const,
      });

      if (!draft.id) throw new Error('Draft ID should be defined');

      const firstResult = await DraftService.markAsCompleted(draft.id);
      const secondResult = await DraftService.markAsCompleted(draft.id);

      // Both should return the same draft
      expect(firstResult?.id).toBe(secondResult?.id);
      expect(secondResult?.status).toBe('completed');
    });
  });

  describe('getCompletedDrafts', () => {
    it('should return only completed drafts', async () => {
      const timestamp = Date.now();

      // Save a completed draft
      await DraftService.saveDraft({
        sessionId: 'session-1',
        title: 'Completed Draft',
        content: '# Done',
        tags: ['done'],
        createdAt: timestamp - 1000,
        status: 'completed' as const,
        completedAt: timestamp,
      });

      // Save a draft draft
      await DraftService.saveDraft({
        sessionId: 'session-2',
        title: 'Draft Draft',
        content: '# In Progress',
        tags: ['wip'],
        createdAt: timestamp,
        status: 'draft' as const,
      });

      const completedDrafts = await DraftService.getCompletedDrafts();

      expect(completedDrafts).toHaveLength(1);
      expect(completedDrafts[0].title).toBe('Completed Draft');
    });

    it('should order completed drafts by completion date (newest first)', async () => {
      const timestamp = Date.now();

      await DraftService.saveDraft({
        sessionId: 'session-1',
        title: 'First Completed',
        content: '# First',
        tags: [],
        createdAt: timestamp - 2000,
        status: 'completed' as const,
        completedAt: timestamp - 2000,
      });

      await DraftService.saveDraft({
        sessionId: 'session-2',
        title: 'Second Completed',
        content: '# Second',
        tags: [],
        createdAt: timestamp - 1000,
        status: 'completed' as const,
        completedAt: timestamp - 1000,
      });

      await DraftService.saveDraft({
        sessionId: 'session-3',
        title: 'Third Completed',
        content: '# Third',
        tags: [],
        createdAt: timestamp,
        status: 'completed' as const,
        completedAt: timestamp,
      });

      const completedDrafts = await DraftService.getCompletedDrafts();

      expect(completedDrafts).toHaveLength(3);
      expect(completedDrafts[0].title).toBe('Third Completed'); // Most recent completion
      expect(completedDrafts[1].title).toBe('Second Completed');
      expect(completedDrafts[2].title).toBe('First Completed');
    });

    it('should return empty array when no completed drafts exist', async () => {
      const completedDrafts = await DraftService.getCompletedDrafts();
      expect(completedDrafts).toEqual([]);
    });

    // Story 3.1: Pagination tests
    it('should support pagination with limit and offset', async () => {
      const timestamp = Date.now();

      // Create 25 completed drafts
      for (let i = 0; i < 25; i++) {
        await DraftService.saveDraft({
          sessionId: `session-${i}`,
          title: `Draft ${i}`,
          content: `# Content ${i}`,
          tags: [],
          createdAt: timestamp - (25 - i) * 1000, // Different creation times
          status: 'completed' as const,
          completedAt: timestamp - (25 - i) * 1000, // Different completion times
        });
      }

      // Get first page (20 items)
      const firstPage = await DraftService.getCompletedDrafts({ limit: 20, offset: 0 });
      expect(firstPage).toHaveLength(20);

      // Get second page (5 remaining items)
      const secondPage = await DraftService.getCompletedDrafts({ limit: 20, offset: 20 });
      expect(secondPage).toHaveLength(5);
    });

    it('should default to limit of 20 when no options provided', async () => {
      const timestamp = Date.now();

      // Create 25 drafts
      for (let i = 0; i < 25; i++) {
        await DraftService.saveDraft({
          sessionId: `session-${i}`,
          title: `Draft ${i}`,
          content: `# Content`,
          tags: [],
          createdAt: timestamp - i * 1000,
          status: 'completed' as const,
          completedAt: timestamp - i * 1000,
        });
      }

      const drafts = await DraftService.getCompletedDrafts();
      expect(drafts).toHaveLength(20); // Default limit
    });
  });

  describe('getCompletedCount', () => {
    // Story 3.1: Count completed drafts
    it('should return count of completed drafts', async () => {
      const timestamp = Date.now();

      // Create some completed drafts
      for (let i = 0; i < 5; i++) {
        await DraftService.saveDraft({
          sessionId: `session-${i}`,
          title: `Completed ${i}`,
          content: '# Content',
          tags: [],
          createdAt: timestamp - i * 1000,
          status: 'completed' as const,
          completedAt: timestamp - i * 1000,
        });
      }

      // Create some draft drafts (should not be counted)
      await DraftService.saveDraft({
        sessionId: 'session-draft',
        title: 'Draft Status',
        content: '# In Progress',
        tags: [],
        createdAt: timestamp,
        status: 'draft' as const,
      });

      const count = await DraftService.getCompletedCount();
      expect(count).toBe(5);
    });

    it('should return 0 when no completed drafts exist', async () => {
      const count = await DraftService.getCompletedCount();
      expect(count).toBe(0);
    });
  });
});
