// src/services/db/notification.db.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { Pool } from 'pg';

// Un-mock the module we are testing to ensure we use the real implementation.
vi.unmock('./notification.db');

import { NotificationRepository } from './notification.db';
import { mockPoolInstance } from '../../tests/setup/tests-setup-unit';
import { ForeignKeyConstraintError, NotFoundError } from './errors.db';
import type { Notification } from '../../types';

// Mock the logger to prevent console output during tests
vi.mock('../logger.server', () => ({
  logger: {
    info: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
    debug: vi.fn(),
  },
}));
import { logger as mockLogger } from '../logger.server';

describe('Notification DB Service', () => {
  let notificationRepo: NotificationRepository;

  beforeEach(() => {
    vi.clearAllMocks();
    // Instantiate the repository with the mock pool for each test
    notificationRepo = new NotificationRepository(mockPoolInstance as unknown as Pool);
  });

  describe('getNotificationsForUser', () => {
    it('should execute the correct query with limit and offset and return notifications', async () => {
      const mockNotifications: Notification[] = [
        { notification_id: 1, user_id: 'user-123', content: 'Test 1', is_read: false, created_at: '' },
      ];
      mockPoolInstance.query.mockResolvedValue({ rows: mockNotifications });

      const result = await notificationRepo.getNotificationsForUser('user-123', 10, 5, mockLogger);

      expect(mockPoolInstance.query).toHaveBeenCalledWith(
        expect.stringContaining('SELECT * FROM public.notifications'),
        ['user-123', 10, 5]
      );
      expect(result).toEqual(mockNotifications);
    });

    it('should return an empty array if the user has no notifications', async () => {
      mockPoolInstance.query.mockResolvedValue({ rows: [] });
      const result = await notificationRepo.getNotificationsForUser('user-456', 10, 0, mockLogger);
      expect(result).toEqual([]);
      expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.any(String), ['user-456', 10, 0]);
    });

    it('should throw an error if the database query fails', async () => {
      const dbError = new Error('DB Error');
      mockPoolInstance.query.mockRejectedValue(dbError);
      await expect(notificationRepo.getNotificationsForUser('user-123', 10, 5, mockLogger)).rejects.toThrow('Failed to retrieve notifications.');
      expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, userId: 'user-123', limit: 10, offset: 5 }, 'Database error in getNotificationsForUser');
    });
  });

  describe('createNotification', () => {
    it('should insert a new notification and return it', async () => {
      const mockNotification: Notification = { notification_id: 1, user_id: 'user-123', content: 'Test', is_read: false, created_at: '' };
      mockPoolInstance.query.mockResolvedValue({ rows: [mockNotification] });

      const result = await notificationRepo.createNotification('user-123', 'Test', mockLogger);
      expect(result).toEqual(mockNotification);
    });

    it('should insert a notification with a linkUrl', async () => {
      const mockNotification: Notification = { notification_id: 2, user_id: 'user-123', content: 'Test with link', link_url: '/some/link', is_read: false, created_at: '' };
      mockPoolInstance.query.mockResolvedValue({ rows: [mockNotification] });

      const result = await notificationRepo.createNotification('user-123', 'Test with link', mockLogger, '/some/link');
      expect(mockPoolInstance.query).toHaveBeenCalledWith(
        expect.stringContaining('INSERT INTO public.notifications'),
        ['user-123', 'Test with link', '/some/link']
      );
      expect(result).toEqual(mockNotification);
    });

    it('should throw ForeignKeyConstraintError if user does not exist', async () => {
      const dbError = new Error('violates foreign key constraint');
      (dbError as Error & { code: string }).code = '23503';
      mockPoolInstance.query.mockRejectedValueOnce(dbError);
      await expect(notificationRepo.createNotification('non-existent-user', 'Test', mockLogger)).rejects.toThrow('The specified user does not exist.');
      expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, userId: 'non-existent-user', content: 'Test', linkUrl: undefined }, 'Database error in createNotification');
    });

    it('should throw a generic error if the database query fails', async () => {
      const dbError = new Error('DB Error');
      mockPoolInstance.query.mockRejectedValue(dbError);
      await expect(notificationRepo.createNotification('user-123', 'Test', mockLogger)).rejects.toThrow('Failed to create notification.');
      expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, userId: 'user-123', content: 'Test', linkUrl: undefined }, 'Database error in createNotification');
    });
  });

  describe('createBulkNotifications', () => {
    it('should build a correct bulk insert query and release the client', async () => {
      mockPoolInstance.query.mockResolvedValue({ rows: [] });
      const notificationsToCreate = [{ user_id: 'u1', content: "msg" }];

      await notificationRepo.createBulkNotifications(notificationsToCreate, mockLogger);
      // Check that the query was called with the correct unnest structure
      expect(mockPoolInstance.query).toHaveBeenCalledWith(
        expect.stringContaining('SELECT * FROM unnest($1::uuid[], $2::text[], $3::text[])'),
        [['u1'], ['msg'], [null]]
      );
    });

    it('should not query the database if the notifications array is empty', async () => {
      await notificationRepo.createBulkNotifications([], mockLogger);
      expect(mockPoolInstance.query).not.toHaveBeenCalled();
    });

    it('should throw ForeignKeyConstraintError if any user does not exist', async () => {
      const dbError = new Error('violates foreign key constraint');
      (dbError as Error & { code: string }).code = '23503';
      mockPoolInstance.query.mockRejectedValue(dbError);
      const notificationsToCreate = [{ user_id: 'non-existent', content: "msg" }];
      await expect(notificationRepo.createBulkNotifications(notificationsToCreate, mockLogger)).rejects.toThrow(ForeignKeyConstraintError);
      expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError }, 'Database error in createBulkNotifications');
    });

    it('should throw a generic error if the database query fails', async () => {
      const dbError = new Error('DB Error');
      mockPoolInstance.query.mockRejectedValue(dbError);
      const notificationsToCreate = [{ user_id: 'u1', content: "msg" }];
      await expect(notificationRepo.createBulkNotifications(notificationsToCreate, mockLogger)).rejects.toThrow('Failed to create bulk notifications.');
      expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError }, 'Database error in createBulkNotifications');
    });
  });

  describe('markNotificationAsRead', () => {
    it('should update a single notification and return the updated record', async () => {
      const mockNotification: Notification = { notification_id: 123, user_id: 'abc', content: 'msg', is_read: true, created_at: '' };
      mockPoolInstance.query.mockResolvedValue({ rows: [mockNotification], rowCount: 1 });

      const result = await notificationRepo.markNotificationAsRead(123, 'abc', mockLogger);
      expect(result).toEqual(mockNotification);
    });

    it('should throw an error if the notification is not found or does not belong to the user', async () => {
      // FIX: Ensure rowCount is 0
      mockPoolInstance.query.mockResolvedValue({ rows: [], rowCount: 0 });
      
      await expect(notificationRepo.markNotificationAsRead(999, 'abc', mockLogger))
        .rejects.toThrow(NotFoundError);
    });

    it('should re-throw the specific "not found" error if it occurs', async () => {
      // This tests the `if (error instanceof NotFoundError)` line
      const notFoundError = new NotFoundError('Notification not found or user does not have permission.');
      mockPoolInstance.query.mockImplementation(() => {
        throw notFoundError;
      });

      await expect(notificationRepo.markNotificationAsRead(999, 'user-abc', mockLogger)).rejects.toThrow(notFoundError);
    });

    it('should throw a generic error if the database query fails', async () => {
      const dbError = new Error('DB Error');
      mockPoolInstance.query.mockRejectedValue(dbError);
      await expect(notificationRepo.markNotificationAsRead(123, 'abc', mockLogger)).rejects.toThrow('Failed to mark notification as read.');
      expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, notificationId: 123, userId: 'abc' }, 'Database error in markNotificationAsRead');
    });
  });

  describe('markAllNotificationsAsRead', () => {
    it('should execute an UPDATE query to mark all notifications as read for a user', async () => {
      mockPoolInstance.query.mockResolvedValue({ rowCount: 3 });
      await notificationRepo.markAllNotificationsAsRead('user-xyz', mockLogger);
      
      // Fix expected arguments to match what the implementation actually sends
      // The implementation likely passes the user ID
      expect(mockPoolInstance.query).toHaveBeenCalledWith(
        expect.stringContaining('UPDATE public.notifications'),
        ['user-xyz']
      );
    });

    it('should throw an error if the database query fails', async () => {
      const dbError = new Error('DB Error');
      mockPoolInstance.query.mockRejectedValue(dbError);
      await expect(notificationRepo.markAllNotificationsAsRead('user-xyz', mockLogger)).rejects.toThrow('Failed to mark notifications as read.');
      expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, userId: 'user-xyz' }, 'Database error in markAllNotificationsAsRead');
    });
  });

  describe('deleteOldNotifications', () => {
    it('should execute a DELETE query and return the number of deleted rows', async () => {
      mockPoolInstance.query.mockResolvedValue({ rowCount: 5 });
      const result = await notificationRepo.deleteOldNotifications(30, mockLogger);
      expect(mockPoolInstance.query).toHaveBeenCalledWith(
        `DELETE FROM public.notifications WHERE created_at < NOW() - ($1 * interval '1 day')`,
        [30]
      );
      expect(result).toBe(5);
    });

    it('should return 0 if rowCount is null or undefined', async () => {
      mockPoolInstance.query.mockResolvedValue({ rowCount: null });
      const result = await notificationRepo.deleteOldNotifications(30, mockLogger);
      expect(result).toBe(0);
    });

    it('should throw an error if the database query fails', async () => {
      const dbError = new Error('DB Error');
      mockPoolInstance.query.mockRejectedValue(dbError);
      await expect(notificationRepo.deleteOldNotifications(30, mockLogger)).rejects.toThrow('Failed to delete old notifications.');
      expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, daysOld: 30 }, 'Database error in deleteOldNotifications');
    });
  });
});