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';

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

const mockList = vi.fn();
const mockFindById = vi.fn();
const mockCreate = vi.fn();
const mockUpdate = vi.fn();
const mockDelete = vi.fn();
const mockAddItem = vi.fn();
const mockUpdateItem = vi.fn();
const mockRemoveItem = vi.fn();

vi.mock('../../../src/modules/shopping-lists/shopping-lists.repository.js', () => ({
  ShoppingListsRepository: class {
    list = mockList;
    findById = mockFindById;
    create = mockCreate;
    update = mockUpdate;
    delete = mockDelete;
    addItem = mockAddItem;
    updateItem = mockUpdateItem;
    removeItem = mockRemoveItem;
  },
}));

vi.mock('../../../src/modules/meal-plans/shopping-gap.service.js', () => ({
  ShoppingGapService: class {
    calculateGap = vi.fn().mockResolvedValue({ missingItems: [] });
  },
}));

vi.mock('../../../src/modules/pantry/pantry.service.js', () => ({
  PantryService: class {
    create = vi.fn().mockResolvedValue({ _id: 'pant1' });
  },
}));

vi.mock('../../../src/modules/products/products.repository.js', () => ({
  ProductsRepository: class {
    findById = vi.fn().mockResolvedValue({ category: 'dairy' });
  },
}));

vi.mock('../../../src/modules/prices/prices.service.js', () => ({
  PricesService: class {
    estimatePrice = vi.fn().mockResolvedValue(5.0);
    recordPrice = vi.fn().mockResolvedValue({});
    compareStores = vi.fn().mockResolvedValue([]);
  },
}));

vi.mock('../../../src/modules/meal-plans/meal-plans.repository.js', () => ({
  MealPlanRepository: class {
    findById = vi.fn().mockResolvedValue({ _id: 'mp1', weekStartDate: new Date() });
    update = vi.fn().mockResolvedValue({});
  },
}));

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 shoppingListsRoutes from '../../../src/modules/shopping-lists/shopping-lists.routes.js';

describe('shopping-lists.routes', () => {
  let app: any;

  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(shoppingListsRoutes);
    await instance.ready();
    return instance;
  }

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

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

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

  function makeShoppingList(overrides = {}) {
    return {
      _id: 'list1',
      householdId: 'hh1',
      name: 'Weekly Checklist',
      items: [],
      status: 'active',
      createdBy: 'kc-1',
      createdAt: new Date(),
      updatedAt: new Date(),
      ...overrides,
    };
  }

  describe('GET /api/v1/households/:householdId/shopping-lists', () => {
    it('returns all lists belonging to household', async () => {
      mockList.mockResolvedValue([makeShoppingList()]);

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

      expect(res.statusCode).toBe(200);
      expect(res.json()).toHaveLength(1);
    });
  });

  describe('POST /api/v1/households/:householdId/shopping-lists', () => {
    it('persists metadata and returns 201 response', async () => {
      mockCreate.mockImplementation(arg => Promise.resolve({ ...arg, _id: 'newListA', createdAt: new Date(), updatedAt: new Date() }));

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/households/hh1/shopping-lists',
        headers: { ...authHeaders, 'content-type': 'application/json' },
        body: JSON.stringify({
          name: 'Costco Run',
          items: []
        }),
      });

      expect(res.statusCode).toBe(201);
      expect(res.json().name).toBe('Costco Run');
    });
  });

  describe('POST /api/v1/households/:householdId/shopping-lists/:id/items', () => {
    it('adds new checklist subdocument item generating tracking UUIDs', async () => {
      mockFindById.mockResolvedValue(makeShoppingList());
      const updated = makeShoppingList({
        items: [{ id: 'itemuuid123', productId: 'p1', quantity: 1, unit: 'piece', checked: false, addedToPantry: false }]
      });
      mockAddItem.mockResolvedValue(updated);

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/households/hh1/shopping-lists/list1/items',
        headers: { ...authHeaders, 'content-type': 'application/json' },
        body: JSON.stringify({
          productId: 'p1',
          quantity: 1,
          unit: 'piece'
        }),
      });

      expect(res.statusCode).toBe(201);
      const body = res.json();
      expect(body.items).toHaveLength(1);
      expect(body.items[0].productId).toBe('p1');
    });
  });

  describe('POST /api/v1/households/:householdId/shopping-lists/:id/sync-to-pantry', () => {
    it('executes batch synchronized promotions resulting in completed summaries', async () => {
      const populatedList = makeShoppingList({
        items: [{ id: 'itemA', productId: 'p1', checked: true, addedToPantry: false, quantity: 1, unit: 'piece' }]
      });
      mockFindById.mockResolvedValue(populatedList);
      mockUpdateItem.mockResolvedValue({});

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/households/hh1/shopping-lists/list1/sync-to-pantry',
        headers: authHeaders,
      });

      expect(res.statusCode).toBe(200);
      const body = res.json();
      expect(body.addedCount).toBe(1);
    });
  });

  describe('GET /api/v1/households/:householdId/shopping-lists/:id', () => {
    it('returns a single shopping list by ID', async () => {
      mockFindById.mockResolvedValue(makeShoppingList());
      const res = await app.inject({
        method: 'GET',
        url: '/api/v1/households/hh1/shopping-lists/list1',
        headers: authHeaders,
      });
      expect(res.statusCode).toBe(200);
      expect(res.json()._id).toBe('list1');
    });
  });

  describe('PATCH /api/v1/households/:householdId/shopping-lists/:id', () => {
    it('updates list metadata', async () => {
      mockFindById.mockResolvedValue(makeShoppingList());
      mockUpdate.mockResolvedValue(makeShoppingList({ name: 'Updated Name' }));
      const res = await app.inject({
        method: 'PATCH',
        url: '/api/v1/households/hh1/shopping-lists/list1',
        headers: { ...authHeaders, 'content-type': 'application/json' },
        body: JSON.stringify({ name: 'Updated Name' }),
      });
      expect(res.statusCode).toBe(200);
      expect(res.json().name).toBe('Updated Name');
    });
  });

  describe('DELETE /api/v1/households/:householdId/shopping-lists/:id', () => {
    it('removes list', async () => {
      mockFindById.mockResolvedValue(makeShoppingList());
      mockDelete.mockResolvedValue(true);
      const res = await app.inject({
        method: 'DELETE',
        url: '/api/v1/households/hh1/shopping-lists/list1',
        headers: authHeaders,
      });
      expect(res.statusCode).toBe(204);
    });
  });

  describe('PATCH /api/v1/households/:householdId/shopping-lists/:id/items/:itemId', () => {
    it('updates item inline and broadcasts differential updates', async () => {
      const item = { id: 'itemA', productId: 'p1', quantity: 2, unit: 'piece', checked: false };
      mockUpdateItem.mockResolvedValue(makeShoppingList({ items: [{ ...item, checked: true }] }));

      const res = await app.inject({
        method: 'PATCH',
        url: '/api/v1/households/hh1/shopping-lists/list1/items/itemA',
        headers: { ...authHeaders, 'content-type': 'application/json' },
        body: JSON.stringify({ checked: true }),
      });

      expect(res.statusCode).toBe(200);
      expect(res.json().items[0].checked).toBe(true);
    });

    it('skips broadcast if item is missing from updated list', async () => {
      // Return a list where itemA is gone (maybe someone else deleted it)
      mockUpdateItem.mockResolvedValue(makeShoppingList({ items: [] }));

      const res = await app.inject({
        method: 'PATCH',
        url: '/api/v1/households/hh1/shopping-lists/list1/items/itemA',
        headers: { ...authHeaders, 'content-type': 'application/json' },
        body: JSON.stringify({ checked: true }),
      });

      expect(res.statusCode).toBe(200);
      expect(res.json().items).toHaveLength(0);
    });
  });

  describe('DELETE /api/v1/households/:householdId/shopping-lists/:id/items/:itemId', () => {
    it('deletes an item from the checklist', async () => {
      mockRemoveItem.mockResolvedValue(makeShoppingList({ items: [] }));

      const res = await app.inject({
        method: 'DELETE',
        url: '/api/v1/households/hh1/shopping-lists/list1/items/itemA',
        headers: authHeaders,
      });

      expect(res.statusCode).toBe(200);
      expect(res.json().items).toHaveLength(0);
    });
  });

  describe('POST /api/v1/households/:householdId/shopping-lists/from-meal-plan/:mealPlanId', () => {
    it('generates dynamic checklist based on scheduled meal gaps', async () => {
      mockCreate.mockResolvedValue(makeShoppingList({ _id: 'generatedList1' }));
      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/households/hh1/shopping-lists/from-meal-plan/mp1',
        headers: authHeaders,
      });
      expect(res.statusCode).toBe(201);
      expect(res.json()._id).toBe('generatedList1');
    });
  });

  describe('GET /api/v1/households/:householdId/shopping-lists/:id/stores', () => {
    it('returns basket store optimization reports', async () => {
      mockFindById.mockResolvedValue(makeShoppingList({ items: [{ productId: 'p1' }] }));
      const res = await app.inject({
        method: 'GET',
        url: '/api/v1/households/hh1/shopping-lists/list1/stores',
        headers: authHeaders,
      });
      expect(res.statusCode).toBe(200);
      expect(res.json().singleStoreOptions).toBeDefined();
    });
  });
});
