import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import Fastify from 'fastify';
import { fastifyAwilixPlugin } from '@fastify/awilix';
import { serializerCompiler, validatorCompiler } from 'fastify-type-provider-zod';
import { OrganizerFillStatus } from '@meshitrack/shared';

vi.mock('jose', () => ({
  createRemoteJWKSet: vi.fn(() => 'mock-jwks'),
  jwtVerify: vi.fn().mockResolvedValue({
    payload: {
      sub: 'kc-1',
      email: 'test@example.com',
      preferred_username: 'testuser',
      realm_access: { roles: ['member'] },
      householdIds: ['hh1'],
    },
    protectedHeader: { alg: 'RS256' },
    key: {},
  }),
}));

const { mockListFills, mockGetFillById, mockPreview, mockFill, mockUndoFill } = vi.hoisted(() => ({
  mockListFills: vi.fn(),
  mockGetFillById: vi.fn(),
  mockPreview: vi.fn(),
  mockFill: vi.fn(),
  mockUndoFill: vi.fn(),
}));

vi.mock('../../../src/modules/organizer/organizer.repository.js', () => ({
  OrganizerRepository: class {
    create = vi.fn();
    update = vi.fn();
    findById = vi.fn();
    findByHousehold = vi.fn();
    updateStatus = vi.fn();
  },
}));

vi.mock('../../../src/modules/organizer/organizer.service.js', () => ({
  OrganizerService: class {
    listFills = mockListFills;
    getFillById = mockGetFillById;
    preview = mockPreview;
    fill = mockFill;
    undoFill = mockUndoFill;
  },
}));

vi.mock('../../../src/modules/users/users.repository.js', () => ({
  UsersRepository: class {
    findByKeycloakId = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
    upsertFromToken = vi.fn().mockResolvedValue({ householdIds: ['hh1'] });
  },
}));

import authPlugin from '../../../src/plugins/auth.plugin.js';
import householdPlugin from '../../../src/plugins/household.plugin.js';
import usersRoutes from '../../../src/modules/users/users.routes.js';
import organizerRoutes from '../../../src/modules/organizer/organizer.routes.js';

function makeFakeFill(overrides = {}) {
  return {
    _id: 'fill-1',
    householdId: 'hh1',
    userId: 'kc-1',
    regimenId: 'reg-1',
    regimenName: 'Daily Medications',
    numberOfDays: 7,
    fillDate: '2024-06-01T00:00:00.000Z',
    items: [
      {
        medicineId: 'med-1',
        medicineName: 'Metformin',
        quantityNeeded: 7,
        quantityTaken: 7,
        wasShort: false,
        shortage: 0,
        deductions: [{ cabinetItemId: 'ci-1', quantityTaken: 7 }],
      },
    ],
    status: OrganizerFillStatus.COMPLETED,
    notes: null,
    createdAt: '2024-06-01T00:00:00.000Z',
    updatedAt: '2024-06-01T00:00:00.000Z',
    ...overrides,
  };
}

function makeFakePreview(overrides = {}) {
  return {
    regimenName: 'Daily Medications',
    numberOfDays: 7,
    items: [
      {
        medicineId: 'med-1',
        medicineName: 'Metformin',
        quantityNeeded: 7,
        quantityAvailable: 30,
        isShort: false,
        shortage: 0,
        cabinetBreakdown: [
          {
            cabinetItemId: 'ci-1',
            expirationDate: '2025-12-31T00:00:00.000Z',
            quantityToTake: 7,
          },
        ],
      },
    ],
    canFillCompletely: true,
    hasShortages: false,
    ...overrides,
  };
}

describe('organizer.routes', () => {
  let app: Awaited<ReturnType<typeof buildTestApp>>;

  async function buildTestApp() {
    const instance = Fastify({ logger: false });
    instance.setValidatorCompiler(validatorCompiler);
    instance.setSerializerCompiler(serializerCompiler);
    await instance.register(fastifyAwilixPlugin, {
      disposeOnClose: true,
      disposeOnResponse: true,
      strictBooleanEnforced: true,
    });
    await instance.register(authPlugin);
    await instance.register(householdPlugin);
    await instance.register(usersRoutes);
    await instance.register(organizerRoutes);
    await instance.ready();
    return instance;
  }

  const authHeaders = { authorization: 'Bearer valid-token' };

  beforeEach(async () => {
    vi.clearAllMocks();
    app = await buildTestApp();
  });

  afterEach(async () => {
    if (app) await app.close();
  });

  describe('GET /api/v1/households/:householdId/organizer/fills', () => {
    it('returns paginated fill list', async () => {
      const fill = makeFakeFill();
      mockListFills.mockResolvedValue({
        data: [fill],
        pagination: { cursor: null, hasMore: false },
      });

      const res = await app.inject({
        method: 'GET',
        url: '/api/v1/households/hh1/organizer/fills',
        headers: authHeaders,
      });

      expect(res.statusCode).toBe(200);
      const body = res.json();
      expect(body.data).toHaveLength(1);
      expect(body.data[0].regimenName).toBe('Daily Medications');
      expect(body.data[0].status).toBe(OrganizerFillStatus.COMPLETED);
      expect(body.pagination.hasMore).toBe(false);
    });

    it('passes query parameters to service', async () => {
      mockListFills.mockResolvedValue({
        data: [],
        pagination: { cursor: null, hasMore: false },
      });

      const res = await app.inject({
        method: 'GET',
        url: '/api/v1/households/hh1/organizer/fills?regimenId=reg-1&status=completed&limit=10',
        headers: authHeaders,
      });

      expect(res.statusCode).toBe(200);
      expect(mockListFills).toHaveBeenCalledWith(
        'hh1',
        'kc-1',
        expect.objectContaining({
          regimenId: 'reg-1',
          status: OrganizerFillStatus.COMPLETED,
          limit: 10,
        }),
      );
    });

    it('handles ObjectId and Date serialization in fill response', async () => {
      const fill = makeFakeFill({
        _id: { toString: () => 'fill-obj' },
        fillDate: new Date('2024-06-01T00:00:00.000Z'),
        createdAt: { toISOString: () => '2024-06-01T00:00:00.000Z' },
        updatedAt: new Date('2024-06-02T00:00:00.000Z'),
      });
      mockListFills.mockResolvedValue({
        data: [fill],
        pagination: { cursor: null, hasMore: false },
      });

      const res = await app.inject({
        method: 'GET',
        url: '/api/v1/households/hh1/organizer/fills',
        headers: authHeaders,
      });

      expect(res.statusCode).toBe(200);
      const body = res.json();
      expect(body.data[0]._id).toBe('fill-obj');
      expect(body.data[0].fillDate).toBe('2024-06-01T00:00:00.000Z');
      expect(body.data[0].createdAt).toBe('2024-06-01T00:00:00.000Z');
      expect(body.data[0].updatedAt).toBe('2024-06-02T00:00:00.000Z');
    });

    it('omits notes from response when null', async () => {
      const fill = makeFakeFill({ notes: null });
      mockListFills.mockResolvedValue({
        data: [fill],
        pagination: { cursor: null, hasMore: false },
      });

      const res = await app.inject({
        method: 'GET',
        url: '/api/v1/households/hh1/organizer/fills',
        headers: authHeaders,
      });

      expect(res.statusCode).toBe(200);
      const body = res.json();
      expect(body.data[0].notes).toBeUndefined();
    });

    it('includes notes in response when present', async () => {
      const fill = makeFakeFill({ notes: 'Refilled before holiday' });
      mockListFills.mockResolvedValue({
        data: [fill],
        pagination: { cursor: null, hasMore: false },
      });

      const res = await app.inject({
        method: 'GET',
        url: '/api/v1/households/hh1/organizer/fills',
        headers: authHeaders,
      });

      expect(res.statusCode).toBe(200);
      const body = res.json();
      expect(body.data[0].notes).toBe('Refilled before holiday');
    });
  });

  describe('GET /api/v1/households/:householdId/organizer/fills/:id', () => {
    it('returns single fill by id', async () => {
      const fill = makeFakeFill();
      mockGetFillById.mockResolvedValue(fill);

      const res = await app.inject({
        method: 'GET',
        url: '/api/v1/households/hh1/organizer/fills/fill-1',
        headers: authHeaders,
      });

      expect(res.statusCode).toBe(200);
      const body = res.json();
      expect(body._id).toBe('fill-1');
      expect(body.regimenId).toBe('reg-1');
      expect(body.items).toHaveLength(1);
      expect(body.items[0].deductions).toHaveLength(1);
      expect(body.items[0].deductions[0].cabinetItemId).toBe('ci-1');
    });

    it('passes id and householdId to service correctly', async () => {
      const fill = makeFakeFill();
      mockGetFillById.mockResolvedValue(fill);

      const res = await app.inject({
        method: 'GET',
        url: '/api/v1/households/hh1/organizer/fills/fill-42',
        headers: authHeaders,
      });

      expect(res.statusCode).toBe(200);
      expect(mockGetFillById).toHaveBeenCalledWith('fill-42', 'hh1');
    });
  });

  describe('POST /api/v1/households/:householdId/organizer/preview', () => {
    it('returns preview result with items and shortage info', async () => {
      const preview = makeFakePreview();
      mockPreview.mockResolvedValue(preview);

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/households/hh1/organizer/preview',
        headers: { ...authHeaders, 'content-type': 'application/json' },
        payload: { regimenId: 'reg-1', numberOfDays: 7 },
      });

      expect(res.statusCode).toBe(200);
      const body = res.json();
      expect(body.regimenName).toBe('Daily Medications');
      expect(body.canFillCompletely).toBe(true);
      expect(body.hasShortages).toBe(false);
      expect(body.items).toHaveLength(1);
      expect(body.items[0].medicineName).toBe('Metformin');
      expect(body.items[0].cabinetBreakdown).toHaveLength(1);
    });

    it('passes regimenId and numberOfDays to service', async () => {
      const preview = makeFakePreview();
      mockPreview.mockResolvedValue(preview);

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/households/hh1/organizer/preview',
        headers: { ...authHeaders, 'content-type': 'application/json' },
        payload: { regimenId: 'reg-99', numberOfDays: 14 },
      });

      expect(res.statusCode).toBe(200);
      expect(mockPreview).toHaveBeenCalledWith('hh1', 'kc-1', 'reg-99', 14);
    });

    it('returns preview with hasShortages=true and isShort items', async () => {
      const preview = makeFakePreview({
        canFillCompletely: false,
        hasShortages: true,
        items: [
          {
            medicineId: 'med-1',
            medicineName: 'Metformin',
            quantityNeeded: 14,
            quantityAvailable: 5,
            isShort: true,
            shortage: 9,
            cabinetBreakdown: [
              {
                cabinetItemId: 'ci-1',
                expirationDate: '2025-12-31T00:00:00.000Z',
                quantityToTake: 5,
              },
            ],
          },
        ],
      });
      mockPreview.mockResolvedValue(preview);

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/households/hh1/organizer/preview',
        headers: { ...authHeaders, 'content-type': 'application/json' },
        payload: { regimenId: 'reg-1', numberOfDays: 14 },
      });

      expect(res.statusCode).toBe(200);
      const body = res.json();
      expect(body.hasShortages).toBe(true);
      expect(body.canFillCompletely).toBe(false);
      expect(body.items[0].isShort).toBe(true);
      expect(body.items[0].shortage).toBe(9);
    });
  });

  describe('POST /api/v1/households/:householdId/organizer/fill', () => {
    it('creates fill and returns 201 with COMPLETED status', async () => {
      const fill = makeFakeFill({ status: OrganizerFillStatus.COMPLETED });
      mockFill.mockResolvedValue(fill);

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/households/hh1/organizer/fill',
        headers: { ...authHeaders, 'content-type': 'application/json' },
        payload: { regimenId: 'reg-1', numberOfDays: 7, allowPartial: false },
      });

      expect(res.statusCode).toBe(201);
      const body = res.json();
      expect(body._id).toBe('fill-1');
      expect(body.status).toBe(OrganizerFillStatus.COMPLETED);
    });

    it('returns 201 with PARTIAL status when wasShort items exist', async () => {
      const fill = makeFakeFill({
        status: OrganizerFillStatus.PARTIAL,
        items: [
          {
            medicineId: 'med-1',
            medicineName: 'Metformin',
            quantityNeeded: 14,
            quantityTaken: 5,
            wasShort: true,
            shortage: 9,
            deductions: [{ cabinetItemId: 'ci-1', quantityTaken: 5 }],
          },
        ],
      });
      mockFill.mockResolvedValue(fill);

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/households/hh1/organizer/fill',
        headers: { ...authHeaders, 'content-type': 'application/json' },
        payload: { regimenId: 'reg-1', numberOfDays: 14, allowPartial: true },
      });

      expect(res.statusCode).toBe(201);
      const body = res.json();
      expect(body.status).toBe(OrganizerFillStatus.PARTIAL);
      expect(body.items[0].wasShort).toBe(true);
      expect(body.items[0].shortage).toBe(9);
    });

    it('returns 400 on invalid body (missing regimenId)', async () => {
      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/households/hh1/organizer/fill',
        headers: { ...authHeaders, 'content-type': 'application/json' },
        payload: { numberOfDays: 7, allowPartial: false },
      });

      expect(res.statusCode).toBe(400);
    });

    it('passes correct args (householdId, userId, body) to service', async () => {
      const fill = makeFakeFill();
      mockFill.mockResolvedValue(fill);

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/households/hh1/organizer/fill',
        headers: { ...authHeaders, 'content-type': 'application/json' },
        payload: { regimenId: 'reg-1', numberOfDays: 7, allowPartial: false, notes: 'test note' },
      });

      expect(res.statusCode).toBe(201);
      expect(mockFill).toHaveBeenCalledWith(
        'hh1',
        'kc-1',
        expect.objectContaining({
          regimenId: 'reg-1',
          numberOfDays: 7,
          allowPartial: false,
          notes: 'test note',
        }),
      );
    });
  });

  describe('POST /api/v1/households/:householdId/organizer/fills/:id/undo', () => {
    it('reverses fill and returns 200 with REVERSED status', async () => {
      const fill = makeFakeFill({ status: OrganizerFillStatus.REVERSED });
      mockUndoFill.mockResolvedValue(fill);

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/households/hh1/organizer/fills/fill-1/undo',
        headers: authHeaders,
      });

      expect(res.statusCode).toBe(200);
      const body = res.json();
      expect(body._id).toBe('fill-1');
      expect(body.status).toBe(OrganizerFillStatus.REVERSED);
    });

    it('passes correct args to service (householdId first, then id, then userId)', async () => {
      const fill = makeFakeFill({ status: OrganizerFillStatus.REVERSED });
      mockUndoFill.mockResolvedValue(fill);

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/households/hh1/organizer/fills/fill-42/undo',
        headers: authHeaders,
      });

      expect(res.statusCode).toBe(200);
      expect(mockUndoFill).toHaveBeenCalledWith('hh1', 'fill-42', 'kc-1');
    });
  });
});
