import { describe, it, expect, vi, beforeEach } from 'vitest';

// Use vi.hoisted so mocks are available in vi.mock factory (which is hoisted)
const { mockLean, mockExec, mockFindOne, mockFindById, mockFindOneAndUpdate, mockSave } =
  vi.hoisted(() => {
    const mockExec = vi.fn();
    const mockLean = vi.fn(() => ({ exec: mockExec }));
    return {
      mockExec,
      mockLean,
      mockFindOne: vi.fn(() => ({ lean: mockLean })),
      mockFindById: vi.fn(() => ({ lean: mockLean })),
      mockFindOneAndUpdate: vi.fn(() => ({ exec: mockExec })),
      mockSave: vi.fn(),
    };
  });

vi.mock('../../../src/schemas/user.schema.js', () => {
  class MockUserModel {
    _data: Record<string, unknown>;
    constructor(data: Record<string, unknown>) {
      this._data = data;
      Object.assign(this, data);
    }
    save() {
      mockSave();
      return Promise.resolve(this);
    }
    toObject() {
      return { _id: 'new-id', ...this._data };
    }
    static findOne = mockFindOne;
    static findById = mockFindById;
    static findOneAndUpdate = mockFindOneAndUpdate;
  }
  return { UserModel: MockUserModel };
});

import { UsersRepository } from '../../../src/modules/users/users.repository.js';

describe('UsersRepository', () => {
  let repo: UsersRepository;

  beforeEach(() => {
    vi.clearAllMocks();
    repo = new UsersRepository();
  });

  describe('findByKeycloakId', () => {
    it('calls findOne with keycloakId and returns lean result', async () => {
      const user = { _id: 'u1', keycloakId: 'kc-1' };
      mockExec.mockResolvedValue(user);

      const result = await repo.findByKeycloakId('kc-1');

      expect(mockFindOne).toHaveBeenCalledWith({ keycloakId: 'kc-1' }, null, {
        session: undefined,
      });
      expect(mockLean).toHaveBeenCalled();
      expect(mockExec).toHaveBeenCalled();
      expect(result).toEqual(user);
    });
  });

  describe('findById', () => {
    it('calls findById and returns lean result', async () => {
      const user = { _id: 'u1' };
      mockExec.mockResolvedValue(user);

      const result = await repo.findById('u1');

      expect(mockFindById).toHaveBeenCalledWith('u1');
      expect(result).toEqual(user);
    });
  });

  describe('create', () => {
    it('creates a new user and returns plain object', async () => {
      mockSave.mockResolvedValue({});

      const data = {
        keycloakId: 'kc-1',
        email: 'test@example.com',
        displayName: 'Test',
        householdIds: [],
      };
      const result = await repo.create(data as never);

      expect(mockSave).toHaveBeenCalled();
      expect(result).toMatchObject({ keycloakId: 'kc-1' });
    });
  });

  describe('update', () => {
    it('calls findOneAndUpdate with $set', async () => {
      const updated = { _id: 'u1', displayName: 'Updated' };
      mockExec.mockResolvedValue(updated);

      const result = await repo.update('kc-1', { displayName: 'Updated' } as never);

      expect(mockFindOneAndUpdate).toHaveBeenCalledWith(
        { keycloakId: 'kc-1' },
        { $set: { displayName: 'Updated' } },
        { new: true, lean: true, session: undefined },
      );
      expect(result).toEqual(updated);
    });
  });

  describe('upsertFromToken', () => {
    it('upserts user with $set and $setOnInsert', async () => {
      const upserted = { _id: 'u1', keycloakId: 'kc-1' };
      mockExec.mockResolvedValue(upserted);

      const result = await repo.upsertFromToken('kc-1', 'a@b.com', 'Name');

      expect(mockFindOneAndUpdate).toHaveBeenCalledWith(
        { keycloakId: 'kc-1' },
        {
          $set: { email: 'a@b.com', displayName: 'Name' },
          $setOnInsert: { keycloakId: 'kc-1', householdIds: [], defaultHouseholdId: null },
        },
        { upsert: true, new: true, lean: true },
      );
      expect(result).toEqual(upserted);
    });
  });
});
