/**
 * Declarative YAML module loader — unit tests.
 */

import { describe, it, expect } from 'vitest';
import Fastify from 'fastify';
import {
  buildCreateSchema,
  buildUpdateSchema,
  defaultEndpoints,
  fieldToZodSchema,
} from './declarative-schema.js';
import { MemoryStore, parseModuleYaml, registerModuleRoutes } from './declarative-loader.js';

// ── Sample YAML ──────────────────────────────────────────────────────────

const SAMPLE_YAML = `
name: announcements
container: announcements
partitionKey: /productId
idPrefix: ann
description: Product announcements
fields:
  title:
    type: string
    required: true
    minLength: 1
    maxLength: 200
  body:
    type: string
    maxLength: 5000
    default: ""
  priority:
    type: enum
    values: [low, medium, high]
    default: medium
  published:
    type: boolean
    default: false
  viewCount:
    type: number
    min: 0
    default: 0
`;

// ── Schema Validation ────────────────────────────────────────────────────

describe('ModuleSchema', () => {
  it('parses valid module definition', () => {
    const mod = parseModuleYaml(SAMPLE_YAML);
    expect(mod.name).toBe('announcements');
    expect(mod.container).toBe('announcements');
    expect(mod.partitionKey).toBe('/productId');
    expect(mod.idPrefix).toBe('ann');
    expect(Object.keys(mod.fields)).toHaveLength(5);
  });

  it('rejects module without name', () => {
    expect(() => parseModuleYaml('container: test\nfields:\n  x:\n    type: string')).toThrow();
  });

  it('rejects module without fields', () => {
    expect(() => parseModuleYaml('name: test\ncontainer: test')).toThrow();
  });

  it('defaults partitionKey to /productId', () => {
    const mod = parseModuleYaml(
      'name: test\ncontainer: test_items\nfields:\n  title:\n    type: string'
    );
    expect(mod.partitionKey).toBe('/productId');
  });

  it('defaults idPrefix to empty string', () => {
    const mod = parseModuleYaml(
      'name: test\ncontainer: test_items\nfields:\n  title:\n    type: string'
    );
    expect(mod.idPrefix).toBe('');
  });
});

// ── Field to Zod ─────────────────────────────────────────────────────────

describe('fieldToZodSchema', () => {
  it('creates string schema with constraints', () => {
    const schema = fieldToZodSchema('name', {
      type: 'string',
      required: true,
      minLength: 1,
      maxLength: 100,
    });
    expect(schema.parse('hello')).toBe('hello');
    expect(() => schema.parse('')).toThrow();
  });

  it('creates number schema with min/max', () => {
    const schema = fieldToZodSchema('count', { type: 'number', required: true, min: 0, max: 100 });
    expect(schema.parse(50)).toBe(50);
    expect(() => schema.parse(-1)).toThrow();
    expect(() => schema.parse(101)).toThrow();
  });

  it('creates boolean schema', () => {
    const schema = fieldToZodSchema('active', { type: 'boolean', required: true });
    expect(schema.parse(true)).toBe(true);
    expect(() => schema.parse('yes')).toThrow();
  });

  it('creates enum schema', () => {
    const schema = fieldToZodSchema('status', {
      type: 'enum',
      required: true,
      values: ['draft', 'published'],
    });
    expect(schema.parse('draft')).toBe('draft');
    expect(() => schema.parse('unknown')).toThrow();
  });

  it('throws for enum without values', () => {
    expect(() => fieldToZodSchema('status', { type: 'enum', required: true })).toThrow();
  });

  it('applies default value', () => {
    const schema = fieldToZodSchema('count', { type: 'number', required: false, default: 0 });
    expect(schema.parse(undefined)).toBe(0);
  });

  it('makes non-required fields optional', () => {
    const schema = fieldToZodSchema('note', { type: 'string', required: false });
    expect(schema.parse(undefined)).toBeUndefined();
  });
});

// ── Build Schemas ────────────────────────────────────────────────────────

describe('buildCreateSchema / buildUpdateSchema', () => {
  const mod = parseModuleYaml(SAMPLE_YAML);

  it('buildCreateSchema validates required fields', () => {
    const schema = buildCreateSchema(mod);
    const result = schema.parse({ title: 'Hello' });
    expect(result.title).toBe('Hello');
    expect(result.body).toBe('');
    expect(result.priority).toBe('medium');
    expect(result.published).toBe(false);
  });

  it('buildCreateSchema rejects missing required field', () => {
    const schema = buildCreateSchema(mod);
    expect(() => schema.parse({ body: 'no title' })).toThrow();
  });

  it('buildUpdateSchema makes all fields optional', () => {
    const schema = buildUpdateSchema(mod);
    const result = schema.parse({ title: 'Updated' });
    expect(result.title).toBe('Updated');
    expect(result.body).toBeUndefined();
  });

  it('buildUpdateSchema accepts empty object', () => {
    const schema = buildUpdateSchema(mod);
    const result = schema.parse({});
    expect(Object.keys(result).length).toBe(0);
  });
});

// ── Default Endpoints ────────────────────────────────────────────────────

describe('defaultEndpoints', () => {
  it('generates 5 CRUD endpoints', () => {
    const mod = parseModuleYaml(SAMPLE_YAML);
    const eps = defaultEndpoints(mod);
    expect(eps).toHaveLength(5);
    expect(eps.map(e => e.method)).toEqual(['GET', 'GET', 'POST', 'PATCH', 'DELETE']);
  });

  it('uses module name as base path', () => {
    const mod = parseModuleYaml(SAMPLE_YAML);
    const eps = defaultEndpoints(mod);
    expect(eps[0].path).toBe('/announcements');
    expect(eps[1].path).toBe('/announcements/:id');
  });

  it('sets DELETE to admin auth', () => {
    const mod = parseModuleYaml(SAMPLE_YAML);
    const eps = defaultEndpoints(mod);
    const deleteEp = eps.find(e => e.method === 'DELETE');
    expect(deleteEp?.auth).toBe('admin');
  });
});

// ── MemoryStore ──────────────────────────────────────────────────────────

describe('MemoryStore', () => {
  it('creates and retrieves documents', async () => {
    const store = new MemoryStore();
    const doc = { id: '1', productId: 'test', title: 'Hello' };
    await store.create('items', doc);
    const found = await store.get('items', '1', 'test');
    expect(found?.title).toBe('Hello');
  });

  it('returns null for non-existent document', async () => {
    const store = new MemoryStore();
    const found = await store.get('items', 'nope', 'test');
    expect(found).toBeNull();
  });

  it('returns null for wrong productId', async () => {
    const store = new MemoryStore();
    await store.create('items', { id: '1', productId: 'a' });
    const found = await store.get('items', '1', 'b');
    expect(found).toBeNull();
  });

  it('lists documents with pagination', async () => {
    const store = new MemoryStore();
    for (let i = 0; i < 10; i++) {
      await store.create('items', {
        id: String(i),
        productId: 'test',
        createdAt: new Date(2026, 0, i + 1).toISOString(),
      });
    }
    const { items, total } = await store.list('items', 'test', { limit: 3, offset: 0 });
    expect(items).toHaveLength(3);
    expect(total).toBe(10);
  });

  it('lists with filters', async () => {
    const store = new MemoryStore();
    await store.create('items', { id: '1', productId: 'test', status: 'active' });
    await store.create('items', { id: '2', productId: 'test', status: 'archived' });
    const { items } = await store.list('items', 'test', { filters: { status: 'active' } });
    expect(items).toHaveLength(1);
    expect(items[0].id).toBe('1');
  });

  it('updates a document', async () => {
    const store = new MemoryStore();
    await store.create('items', { id: '1', productId: 'test', title: 'Old' });
    const updated = await store.update('items', '1', 'test', { title: 'New' });
    expect(updated?.title).toBe('New');
    expect(updated?.updatedAt).toBeDefined();
  });

  it('returns null when updating non-existent', async () => {
    const store = new MemoryStore();
    const result = await store.update('items', 'nope', 'test', { title: 'x' });
    expect(result).toBeNull();
  });

  it('deletes a document', async () => {
    const store = new MemoryStore();
    await store.create('items', { id: '1', productId: 'test' });
    const ok = await store.delete('items', '1', 'test');
    expect(ok).toBe(true);
    const found = await store.get('items', '1', 'test');
    expect(found).toBeNull();
  });

  it('returns false when deleting non-existent', async () => {
    const store = new MemoryStore();
    const ok = await store.delete('items', 'nope', 'test');
    expect(ok).toBe(false);
  });
});

// ── Route Integration ────────────────────────────────────────────────────

describe('registerModuleRoutes', () => {
  async function buildTestApp() {
    const app = Fastify({ logger: false });
    // Minimal JWT extraction decorator
    app.decorateRequest('jwtPayload', null);
    app.addHook('preHandler', async req => {
      const auth = req.headers.authorization;
      if (auth?.startsWith('Bearer ')) {
        try {
          const payload = JSON.parse(Buffer.from(auth.split('.')[1], 'base64').toString());
          (req as unknown as { jwtPayload: unknown }).jwtPayload = payload;
        } catch {
          /* no-op */
        }
      }
    });
    // Error handler that respects statusCode on thrown errors
    app.setErrorHandler((err: Error & { statusCode?: number }, _req, reply) => {
      const code = err.statusCode ?? 500;
      reply.status(code).send({ error: err.message });
    });
    return app;
  }

  function makeJwt(sub: string, role: string = 'user'): string {
    const header = Buffer.from(JSON.stringify({ alg: 'none' })).toString('base64');
    const payload = Buffer.from(JSON.stringify({ sub, role })).toString('base64');
    return `Bearer ${header}.${payload}.sig`;
  }

  const HEADERS = {
    'x-product-id': 'testapp',
    'content-type': 'application/json',
    authorization: makeJwt('user-1'),
  };

  const ADMIN_HEADERS = {
    ...HEADERS,
    authorization: makeJwt('admin-1', 'admin'),
  };

  it('registers CRUD routes from module definition', async () => {
    const app = await buildTestApp();
    const store = new MemoryStore();
    const mod = parseModuleYaml(SAMPLE_YAML);
    registerModuleRoutes(app, mod, store);
    await app.ready();

    // CREATE
    const createRes = await app.inject({
      method: 'POST',
      url: '/announcements',
      headers: HEADERS,
      payload: { title: 'Launch Day' },
    });
    expect(createRes.statusCode).toBe(201);
    const created = createRes.json();
    expect(created.title).toBe('Launch Day');
    expect(created.id).toMatch(/^ann_/);
    expect(created.productId).toBe('testapp');
    expect(created.priority).toBe('medium');

    // LIST
    const listRes = await app.inject({
      method: 'GET',
      url: '/announcements',
      headers: HEADERS,
    });
    expect(listRes.statusCode).toBe(200);
    const listed = listRes.json();
    expect(listed.items).toHaveLength(1);
    expect(listed.total).toBe(1);

    // GET BY ID
    const getRes = await app.inject({
      method: 'GET',
      url: `/announcements/${created.id}`,
      headers: HEADERS,
    });
    expect(getRes.statusCode).toBe(200);
    expect(getRes.json().title).toBe('Launch Day');

    // UPDATE
    const updateRes = await app.inject({
      method: 'PATCH',
      url: `/announcements/${created.id}`,
      headers: HEADERS,
      payload: { title: 'Updated Title', published: true },
    });
    expect(updateRes.statusCode).toBe(200);
    expect(updateRes.json().title).toBe('Updated Title');
    expect(updateRes.json().published).toBe(true);

    // DELETE (requires admin) — omit content-type for DELETE
    const { 'content-type': _ignoreCtUser, ...deleteUserHeaders } = HEADERS;
    const deleteUserRes = await app.inject({
      method: 'DELETE',
      url: `/announcements/${created.id}`,
      headers: deleteUserHeaders,
    });
    expect(deleteUserRes.statusCode).toBe(403);

    const { 'content-type': _ignoreCtAdmin, ...deleteAdminHeaders } = ADMIN_HEADERS;
    const deleteAdminRes = await app.inject({
      method: 'DELETE',
      url: `/announcements/${created.id}`,
      headers: deleteAdminHeaders,
    });
    expect(deleteAdminRes.statusCode).toBe(204);

    // Verify deleted
    const getDeletedRes = await app.inject({
      method: 'GET',
      url: `/announcements/${created.id}`,
      headers: HEADERS,
    });
    expect(getDeletedRes.statusCode).toBe(404);

    await app.close();
  });

  it('rejects requests without auth', async () => {
    const app = await buildTestApp();
    const store = new MemoryStore();
    const mod = parseModuleYaml(SAMPLE_YAML);
    registerModuleRoutes(app, mod, store);
    await app.ready();

    const res = await app.inject({
      method: 'GET',
      url: '/announcements',
      headers: { 'x-product-id': 'testapp' }, // no auth
    });
    expect(res.statusCode).toBe(401);
    await app.close();
  });

  it('rejects requests without x-product-id', async () => {
    const app = await buildTestApp();
    const store = new MemoryStore();
    const mod = parseModuleYaml(SAMPLE_YAML);
    registerModuleRoutes(app, mod, store);
    await app.ready();

    const res = await app.inject({
      method: 'GET',
      url: '/announcements',
      headers: { authorization: makeJwt('user-1') }, // no product id
    });
    expect(res.statusCode).toBe(400);
    await app.close();
  });

  it('validates create input against schema', async () => {
    const app = await buildTestApp();
    const store = new MemoryStore();
    const mod = parseModuleYaml(SAMPLE_YAML);
    registerModuleRoutes(app, mod, store);
    await app.ready();

    // Missing required 'title'
    const res = await app.inject({
      method: 'POST',
      url: '/announcements',
      headers: HEADERS,
      payload: { body: 'no title field' },
    });
    // Zod will throw, Fastify returns 500 by default (or 400 with proper error handler)
    expect(res.statusCode).toBeGreaterThanOrEqual(400);
    await app.close();
  });

  it('supports custom endpoints in YAML', () => {
    const yaml = `
name: widgets
container: widgets
fields:
  label:
    type: string
    required: true
endpoints:
  - method: GET
    path: /widgets
    auth: none
  - method: POST
    path: /widgets/import
    auth: admin
    custom: true
`;
    const mod = parseModuleYaml(yaml);
    expect(mod.endpoints).toHaveLength(2);
    expect(mod.endpoints![1].custom).toBe(true);
  });

  it('supports auth: none endpoints', async () => {
    const yaml = `
name: public-items
container: public_items
fields:
  title:
    type: string
    required: true
endpoints:
  - method: GET
    path: /public-items
    auth: none
`;
    const app = await buildTestApp();
    const store = new MemoryStore();
    const mod = parseModuleYaml(yaml);
    registerModuleRoutes(app, mod, store);
    await app.ready();

    // No auth needed
    const res = await app.inject({
      method: 'GET',
      url: '/public-items',
      headers: { 'x-product-id': 'testapp' },
    });
    expect(res.statusCode).toBe(200);
    await app.close();
  });
});
