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 { HouseholdRole } from '@meshitrack/shared';

// Mock jose for auth plugin
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: {},
  }),
}));

// Hoist mock fns
const {
  mockCreate,
  mockFindById,
  mockUpdate,
  mockUpdateInviteCode,
  mockFindByInviteCode,
  mockAddMember,
  mockFindByKeycloakId,
  mockUserUpdate,
} = vi.hoisted(() => ({
  mockCreate: vi.fn(),
  mockFindById: vi.fn(),
  mockUpdate: vi.fn(),
  mockUpdateInviteCode: vi.fn(),
  mockFindByInviteCode: vi.fn(),
  mockAddMember: vi.fn(),
  mockFindByKeycloakId: vi.fn(),
  mockUserUpdate: vi.fn(),
}));

vi.mock('../../../src/modules/households/households.repository.js', () => ({
  HouseholdsRepository: class {
    create = mockCreate;
    findById = mockFindById;
    update = mockUpdate;
    updateInviteCode = mockUpdateInviteCode;
    findByInviteCode = mockFindByInviteCode;
    addMember = mockAddMember;
  },
}));

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

vi.mock('uuid', () => ({
  v4: vi.fn(() => '12345678-1234-1234-1234-123456789abc'),
}));

vi.mock('mongoose', () => ({
  default: {
    startSession: vi.fn().mockResolvedValue({
      startTransaction: vi.fn(),
      commitTransaction: vi.fn().mockResolvedValue(undefined),
      abortTransaction: vi.fn().mockResolvedValue(undefined),
      endSession: vi.fn(),
    }),
  },
}));

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 householdsRoutes from '../../../src/modules/households/households.routes.js';

function makeFakeHousehold(overrides = {}) {
  return {
    _id: 'hh1',
    name: 'Test Household',
    ownerUserId: 'kc-1',
    members: [{ userId: 'kc-1', role: HouseholdRole.OWNER, joinedAt: new Date().toISOString() }],
    inviteCode: '12345678',
    settings: { timezone: 'UTC', currency: 'USD', language: 'en' },
    createdAt: new Date().toISOString(),
    updatedAt: new Date().toISOString(),
    ...overrides,
  };
}

describe('households.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(householdsRoutes);
    await instance.ready();
    return instance;
  }

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

  beforeEach(async () => {
    vi.clearAllMocks();
    // Default: auth plugin finds user with hh1 membership (routes with householdId guard pass)
    mockFindByKeycloakId.mockResolvedValue({ householdIds: ['hh1'], defaultHouseholdId: null });
    app = await buildTestApp();
  });

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

  describe('POST /api/v1/households', () => {
    it('creates a household', async () => {
      const household = makeFakeHousehold();
      mockCreate.mockResolvedValue(household);
      mockFindByKeycloakId.mockResolvedValue({ householdIds: [], defaultHouseholdId: null });
      mockUserUpdate.mockResolvedValue({});

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/households',
        headers: authHeaders,
        payload: { name: 'Test Household' },
      });

      expect(res.statusCode).toBe(201);
      expect(res.json().name).toBe('Test Household');
    });
  });

  describe('POST /api/v1/households (ObjectId/Date conversion)', () => {
    it('handles ObjectId and Date objects in response', async () => {
      const household = makeFakeHousehold({
        _id: { toString: () => 'hh-obj' },
        createdAt: { toISOString: () => '2024-01-01T00:00:00.000Z' },
        updatedAt: { toISOString: () => '2024-01-02T00:00:00.000Z' },
        members: [
          {
            userId: 'kc-1',
            role: HouseholdRole.OWNER,
            joinedAt: { toISOString: () => '2024-01-01T00:00:00.000Z' },
          },
        ],
        settings: null,
      });
      mockCreate.mockResolvedValue(household);
      mockFindByKeycloakId.mockResolvedValue({ householdIds: [], defaultHouseholdId: null });
      mockUserUpdate.mockResolvedValue({});

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/households',
        headers: authHeaders,
        payload: { name: 'Test' },
      });

      expect(res.statusCode).toBe(201);
      const body = res.json();
      expect(body._id).toBe('hh-obj');
      expect(body.createdAt).toBe('2024-01-01T00:00:00.000Z');
      expect(body.members[0].joinedAt).toBe('2024-01-01T00:00:00.000Z');
      expect(body.settings.timezone).toBe('UTC');
      expect(body.settings.currency).toBe('USD');
      expect(body.settings.language).toBe('en');
    });
  });

  describe('GET /api/v1/households/:id', () => {
    it('returns a household', async () => {
      const household = makeFakeHousehold();
      mockFindById.mockResolvedValue(household);

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

      expect(res.statusCode).toBe(200);
      expect(res.json().name).toBe('Test Household');
    });
  });

  describe('PATCH /api/v1/households/:id', () => {
    it('updates a household', async () => {
      const household = makeFakeHousehold();
      mockFindById.mockResolvedValue(household);
      mockUpdate.mockResolvedValue({ ...household, name: 'Updated' });

      const res = await app.inject({
        method: 'PATCH',
        url: '/api/v1/households/hh1',
        headers: authHeaders,
        payload: { name: 'Updated' },
      });

      expect(res.statusCode).toBe(200);
      expect(res.json().name).toBe('Updated');
    });
  });

  describe('POST /api/v1/households/:id/invite', () => {
    it('generates a new invite code', async () => {
      const household = makeFakeHousehold();
      mockFindById.mockResolvedValue(household);
      mockUpdateInviteCode.mockResolvedValue({ ...household, inviteCode: '12345678' });

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

      expect(res.statusCode).toBe(200);
      expect(res.json().inviteCode).toBeDefined();
    });
  });

  describe('POST /api/v1/households/join', () => {
    it('joins a household via invite code', async () => {
      const household = makeFakeHousehold({
        members: [
          { userId: 'kc-other', role: HouseholdRole.OWNER, joinedAt: new Date().toISOString() },
        ],
      });
      mockFindByInviteCode.mockResolvedValue(household);
      mockAddMember.mockResolvedValue({
        ...household,
        members: [
          ...household.members,
          { userId: 'kc-1', role: HouseholdRole.MEMBER, joinedAt: new Date().toISOString() },
        ],
      });
      mockFindByKeycloakId.mockResolvedValue({ householdIds: [], defaultHouseholdId: null });
      mockUserUpdate.mockResolvedValue({});

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/households/join',
        headers: authHeaders,
        payload: { inviteCode: '12345678' },
      });

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