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

// Mock mongoose to prevent real DB connections; provide class-based models
vi.mock('mongoose', () => {
  class FakeSchema {
    paths: Record<string, unknown> = {};
    constructor(def: Record<string, unknown>, _opts?: unknown) {
      for (const key of Object.keys(def)) {
        this.paths[key] = { path: key };
      }
      this.paths['createdAt'] = { path: 'createdAt' };
      this.paths['updatedAt'] = { path: 'updatedAt' };
    }
    index() {
      return this;
    }
  }

  const models: Record<string, unknown> = {};

  function createFakeModel(name: string) {
    const mockExec = vi.fn().mockResolvedValue(null);
    const mockLean = vi.fn(() => ({ exec: mockExec }));

    class Model {
      _data: Record<string, unknown>;
      constructor(data: Record<string, unknown>) {
        this._data = data;
        Object.assign(this, data);
      }
      save() {
        return Promise.resolve(this);
      }
      toObject() {
        return { _id: `${name}-id`, ...this._data };
      }
      static modelName = name;
      static schema = new FakeSchema({});
      static findOne = vi.fn(() => ({ lean: mockLean }));
      static findById = vi.fn(() => ({ lean: mockLean }));
      static findOneAndUpdate = vi.fn(() => ({ exec: mockExec }));
      static findByIdAndUpdate = vi.fn(() => ({ exec: mockExec }));
    }

    return Model;
  }

  return {
    default: {
      Schema: FakeSchema,
      model: vi.fn((name: string, _schema?: unknown) => {
        if (!models[name]) models[name] = createFakeModel(name);
        return models[name];
      }),
      connect: vi.fn().mockResolvedValue(undefined),
      disconnect: vi.fn().mockResolvedValue(undefined),
    },
  };
});

vi.mock('jose', () => ({
  createRemoteJWKSet: vi.fn(() => 'mock-jwks'),
  jwtVerify: vi.fn(),
}));

import { buildApp } from '../src/main.js';
import * as jose from 'jose';
import { NotFoundError } from '../src/common/errors.js';

describe('buildApp', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('creates a Fastify app that is ready', async () => {
    const app = await buildApp({ logger: false });
    await app.ready();
    expect(app).toBeDefined();
    await app.close();
  });

  it('has the health route available', async () => {
    const app = await buildApp({ logger: false });
    await app.ready();

    const res = await app.inject({ method: 'GET', url: '/api/v1/health' });
    expect(res.statusCode).toBe(200);
    expect(res.json().status).toBe('ok');

    await app.close();
  });
});

describe('error handler', () => {
  async function getApp() {
    // Set up a valid JWT mock for authenticated routes
    vi.mocked(jose.jwtVerify).mockResolvedValue({
      payload: {
        sub: 'kc-1',
        email: 'test@example.com',
        preferred_username: 'testuser',
        realm_access: { roles: ['member'] },
        householdIds: ['hh1'],
      },
      protectedHeader: { alg: 'RS256' },
      key: {} as never,
    } as never);

    const app = await buildApp({ logger: false });

    // Register test routes that throw various errors
    app.get('/test/app-error', { config: { public: true } as never }, async () => {
      throw new NotFoundError('Test not found');
    });

    app.get('/test/generic-error', { config: { public: true } as never }, async () => {
      const err = new Error('Something broke');
      (err as unknown as Record<string, unknown>).statusCode = 422;
      throw err;
    });

    app.get('/test/unknown-error', { config: { public: true } as never }, async () => {
      throw new Error('Unexpected');
    });

    await app.ready();
    return app;
  }

  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('handles AppError with correct status and body', async () => {
    const app = await getApp();

    const res = await app.inject({ method: 'GET', url: '/test/app-error' });

    expect(res.statusCode).toBe(404);
    const body = res.json();
    expect(body.error).toBe('Not Found');
    expect(body.message).toBe('Test not found');
    expect(body.timestamp).toBeDefined();
    expect(body.path).toBe('/test/app-error');

    await app.close();
  });

  it('handles generic errors with statusCode', async () => {
    const app = await getApp();

    const res = await app.inject({ method: 'GET', url: '/test/generic-error' });

    expect(res.statusCode).toBe(422);
    const body = res.json();
    expect(body.error).toBe('Error');
    expect(body.message).toBe('Something broke');

    await app.close();
  });

  it('handles unknown 500 errors without leaking messages', async () => {
    const app = await getApp();

    const res = await app.inject({ method: 'GET', url: '/test/unknown-error' });

    expect(res.statusCode).toBe(500);
    const body = res.json();
    expect(body.error).toBe('Internal Server Error');
    expect(body.message).toBe('An unexpected error occurred');

    await app.close();
  });

  it('returns 404 for unknown routes', async () => {
    const app = await getApp();

    const res = await app.inject({
      method: 'GET',
      url: '/nonexistent',
      headers: {
        authorization: 'Bearer test-token',
        'x-household-id': 'hh1',
      },
    });

    expect(res.statusCode).toBe(404);

    await app.close();
  });
});
