// src/services/db/reaction.db.test.ts
import { describe, it, expect, vi, beforeEach, Mock } from 'vitest';
import type { PoolClient } from 'pg';
import { ReactionRepository } from './reaction.db';
import { mockPoolInstance } from '../../tests/setup/tests-setup-unit';
import { withTransaction } from './connection.db';
import { ForeignKeyConstraintError } from './errors.db';
import type { UserReaction } from '../../types';

// Un-mock the module we are testing
vi.unmock('./reaction.db');

// Mock dependencies
vi.mock('../logger.server', () => ({
  logger: {
    info: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
    debug: vi.fn(),
  },
}));
import { logger as mockLogger } from '../logger.server';

vi.mock('./connection.db', async (importOriginal) => {
  const actual = await importOriginal<typeof import('./connection.db')>();
  return { ...actual, withTransaction: vi.fn() };
});

describe('Reaction DB Service', () => {
  let reactionRepo: ReactionRepository;
  const mockDb = {
    query: vi.fn(),
  };

  beforeEach(() => {
    vi.clearAllMocks();
    mockDb.query.mockReset();
    reactionRepo = new ReactionRepository(mockDb);
  });

  describe('getReactions', () => {
    it('should build a query with no filters', async () => {
      mockDb.query.mockResolvedValue({ rows: [] });
      await reactionRepo.getReactions({}, mockLogger);
      expect(mockDb.query).toHaveBeenCalledWith(
        'SELECT * FROM public.user_reactions WHERE 1=1 ORDER BY created_at DESC',
        [],
      );
    });

    it('should build a query with a userId filter', async () => {
      mockDb.query.mockResolvedValue({ rows: [] });
      await reactionRepo.getReactions({ userId: 'user-1' }, mockLogger);
      expect(mockDb.query).toHaveBeenCalledWith(
        'SELECT * FROM public.user_reactions WHERE 1=1 AND user_id = $1 ORDER BY created_at DESC',
        ['user-1'],
      );
    });

    it('should build a query with all filters', async () => {
      mockDb.query.mockResolvedValue({ rows: [] });
      await reactionRepo.getReactions(
        { userId: 'user-1', entityType: 'recipe', entityId: '123' },
        mockLogger,
      );
      expect(mockDb.query).toHaveBeenCalledWith(
        'SELECT * FROM public.user_reactions WHERE 1=1 AND user_id = $1 AND entity_type = $2 AND entity_id = $3 ORDER BY created_at DESC',
        ['user-1', 'recipe', '123'],
      );
    });

    it('should return an array of reactions on success', async () => {
      const mockReactions: UserReaction[] = [
        {
          reaction_id: 1,
          user_id: 'user-1',
          entity_type: 'recipe',
          entity_id: '123',
          reaction_type: 'like',
          created_at: new Date().toISOString(),
          updated_at: new Date().toISOString(),
        },
      ];
      mockDb.query.mockResolvedValue({ rows: mockReactions });
      const result = await reactionRepo.getReactions({}, mockLogger);
      expect(result).toEqual(mockReactions);
    });

    it('should throw an error if the database query fails', async () => {
      const dbError = new Error('DB Error');
      mockDb.query.mockRejectedValue(dbError);
      await expect(reactionRepo.getReactions({}, mockLogger)).rejects.toThrow(
        'Failed to retrieve user reactions.',
      );
      expect(mockLogger.error).toHaveBeenCalledWith(
        { err: dbError, filters: {} },
        'Database error in getReactions',
      );
    });
  });

  describe('toggleReaction', () => {
    const reactionData = {
      user_id: 'user-1',
      entity_type: 'recipe',
      entity_id: '123',
      reaction_type: 'like',
    };

    it('should remove an existing reaction and return null', async () => {
      const mockClient = { query: vi.fn() };
      // Mock DELETE returning 1 row, indicating a reaction was deleted
      (mockClient.query as Mock).mockResolvedValueOnce({ rowCount: 1 });

      vi.mocked(withTransaction).mockImplementation(async (callback) => {
        return callback(mockClient as unknown as PoolClient);
      });

      const result = await reactionRepo.toggleReaction(reactionData, mockLogger);

      expect(result).toBeNull();
      expect(mockClient.query).toHaveBeenCalledWith(
        'DELETE FROM public.user_reactions WHERE user_id = $1 AND entity_type = $2 AND entity_id = $3 AND reaction_type = $4',
        ['user-1', 'recipe', '123', 'like'],
      );
      // Ensure INSERT was not called
      expect(mockClient.query).toHaveBeenCalledTimes(1);
    });

    it('should add a new reaction and return it if it does not exist', async () => {
      const mockClient = { query: vi.fn() };
      const mockCreatedReaction: UserReaction = {
        reaction_id: 1,
        ...reactionData,
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
      };

      // Mock DELETE returning 0 rows, then mock INSERT returning the new reaction
      (mockClient.query as Mock)
        .mockResolvedValueOnce({ rowCount: 0 }) // DELETE
        .mockResolvedValueOnce({ rows: [mockCreatedReaction] }); // INSERT

      vi.mocked(withTransaction).mockImplementation(async (callback) => {
        return callback(mockClient as unknown as PoolClient);
      });

      const result = await reactionRepo.toggleReaction(reactionData, mockLogger);

      expect(result).toEqual(mockCreatedReaction);
      expect(mockClient.query).toHaveBeenCalledTimes(2);
      expect(mockClient.query).toHaveBeenCalledWith(
        'INSERT INTO public.user_reactions (user_id, entity_type, entity_id, reaction_type) VALUES ($1, $2, $3, $4) RETURNING *',
        ['user-1', 'recipe', '123', 'like'],
      );
    });

    it('should treat null rowCount as 0 and add a new reaction', async () => {
      const mockClient = { query: vi.fn() };
      const mockCreatedReaction: UserReaction = {
        reaction_id: 2,
        ...reactionData,
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
      };

      // Mock DELETE returning null rowCount (edge case), then INSERT
      (mockClient.query as Mock)
        .mockResolvedValueOnce({ rowCount: null }) // DELETE with null rowCount
        .mockResolvedValueOnce({ rows: [mockCreatedReaction] }); // INSERT

      vi.mocked(withTransaction).mockImplementation(async (callback) => {
        return callback(mockClient as unknown as PoolClient);
      });

      const result = await reactionRepo.toggleReaction(reactionData, mockLogger);

      expect(result).toEqual(mockCreatedReaction);
      expect(mockClient.query).toHaveBeenCalledTimes(2);
    });

    it('should throw ForeignKeyConstraintError if user or entity does not exist', async () => {
      const dbError = new Error('violates foreign key constraint');
      (dbError as Error & { code: string }).code = '23503';

      vi.mocked(withTransaction).mockImplementation(async (callback) => {
        const mockClient = { query: vi.fn().mockRejectedValue(dbError) };
        await expect(callback(mockClient as unknown as PoolClient)).rejects.toThrow(dbError);
        throw dbError;
      });

      await expect(reactionRepo.toggleReaction(reactionData, mockLogger)).rejects.toThrow(
        ForeignKeyConstraintError,
      );
      await expect(reactionRepo.toggleReaction(reactionData, mockLogger)).rejects.toThrow(
        'The specified user or entity does not exist.',
      );
    });

    it('should throw a generic error if the transaction fails', async () => {
      const dbError = new Error('Transaction failed');
      vi.mocked(withTransaction).mockRejectedValue(dbError);

      await expect(reactionRepo.toggleReaction(reactionData, mockLogger)).rejects.toThrow(
        'Failed to toggle user reaction.',
      );
      expect(mockLogger.error).toHaveBeenCalledWith(
        { err: dbError, reactionData },
        'Database error in toggleReaction',
      );
    });
  });

  describe('getReactionSummary', () => {
    it('should return a summary of reactions for an entity', async () => {
      const mockSummary = [
        { reaction_type: 'like', count: 5 },
        { reaction_type: 'heart', count: 2 },
      ];
      // This method uses getPool() directly, so we mock the main instance
      mockPoolInstance.query.mockResolvedValue({ rows: mockSummary });

      const result = await reactionRepo.getReactionSummary('recipe', '123', mockLogger);

      expect(result).toEqual(mockSummary);
      expect(mockPoolInstance.query).toHaveBeenCalledWith(
        expect.stringContaining('GROUP BY reaction_type'),
        ['recipe', '123'],
      );
    });

    it('should return an empty array if there are no reactions', async () => {
      mockPoolInstance.query.mockResolvedValue({ rows: [] });
      const result = await reactionRepo.getReactionSummary('recipe', '456', mockLogger);
      expect(result).toEqual([]);
    });

    it('should throw an error if the database query fails', async () => {
      const dbError = new Error('DB Error');
      mockPoolInstance.query.mockRejectedValue(dbError);
      await expect(reactionRepo.getReactionSummary('recipe', '123', mockLogger)).rejects.toThrow(
        'Failed to retrieve reaction summary.',
      );
      expect(mockLogger.error).toHaveBeenCalledWith(
        { err: dbError, entityType: 'recipe', entityId: '123' },
        'Database error in getReactionSummary',
      );
    });
  });
});
