/**
 * Unit tests for federation wire-format DTOs.
 *
 * Coverage:
 *  - FederationRequestSchema  (valid + invalid)
 *  - FederationListResponseSchema factory
 *  - FederationGetResponseSchema factory
 *  - FederationCapabilitiesResponseSchema
 *  - FederationErrorEnvelopeSchema + error code exhaustiveness
 *  - FederationError exception hierarchy
 *  - tagWithSource helper round-trip
 *  - SourceTagSchema
 */

import { describe, expect, it } from 'vitest';
import { z } from 'zod';

import {
  FEDERATION_ERROR_CODES,
  FEDERATION_VERBS,
  FederationCapabilitiesResponseSchema,
  FederationError,
  FederationErrorEnvelopeSchema,
  FederationForbiddenError,
  FederationInternalError,
  FederationInvalidRequestError,
  FederationNotFoundError,
  FederationRateLimitedError,
  FederationRequestSchema,
  FederationScopeViolationError,
  FederationUnauthorizedError,
  FederationGetResponseSchema,
  FederationListResponseSchema,
  SOURCE_LOCAL,
  SourceTagSchema,
  parseFederationErrorEnvelope,
  tagWithSource,
} from '../index.js';

// ---------------------------------------------------------------------------
// Verbs
// ---------------------------------------------------------------------------

describe('FEDERATION_VERBS', () => {
  it('contains exactly list, get, capabilities', () => {
    expect(FEDERATION_VERBS).toEqual(['list', 'get', 'capabilities']);
  });
});

// ---------------------------------------------------------------------------
// FederationRequestSchema
// ---------------------------------------------------------------------------

describe('FederationRequestSchema', () => {
  it('accepts a minimal valid list request', () => {
    const result = FederationRequestSchema.safeParse({ verb: 'list', resource: 'tasks' });
    expect(result.success).toBe(true);
  });

  it('accepts a get request with cursor and params', () => {
    const result = FederationRequestSchema.safeParse({
      verb: 'get',
      resource: 'notes',
      cursor: 'abc123',
      params: { filter: 'mine' },
    });
    expect(result.success).toBe(true);
    if (result.success) {
      expect(result.data.cursor).toBe('abc123');
      expect(result.data.params?.['filter']).toBe('mine');
    }
  });

  it('accepts a capabilities request', () => {
    const result = FederationRequestSchema.safeParse({ verb: 'capabilities', resource: 'tasks' });
    expect(result.success).toBe(true);
  });

  it('rejects an unknown verb', () => {
    const result = FederationRequestSchema.safeParse({ verb: 'search', resource: 'tasks' });
    expect(result.success).toBe(false);
  });

  it('rejects an empty resource string', () => {
    const result = FederationRequestSchema.safeParse({ verb: 'list', resource: '' });
    expect(result.success).toBe(false);
  });

  it('rejects a missing verb', () => {
    const result = FederationRequestSchema.safeParse({ resource: 'tasks' });
    expect(result.success).toBe(false);
  });
});

// ---------------------------------------------------------------------------
// FederationListResponseSchema factory
// ---------------------------------------------------------------------------

describe('FederationListResponseSchema', () => {
  const ItemSchema = z.object({ id: z.string(), name: z.string() });
  const ListSchema = FederationListResponseSchema(ItemSchema);

  it('accepts a valid list envelope', () => {
    const result = ListSchema.safeParse({
      items: [{ id: '1', name: 'Task A' }],
      nextCursor: 'page2',
      _partial: false,
      _truncated: false,
    });
    expect(result.success).toBe(true);
    if (result.success) {
      expect(result.data.items).toHaveLength(1);
      expect(result.data.nextCursor).toBe('page2');
    }
  });

  it('accepts a minimal envelope with empty items', () => {
    const result = ListSchema.safeParse({ items: [] });
    expect(result.success).toBe(true);
  });

  it('rejects when items is missing', () => {
    const result = ListSchema.safeParse({ nextCursor: 'x' });
    expect(result.success).toBe(false);
  });

  it('rejects when an item fails validation', () => {
    const result = ListSchema.safeParse({ items: [{ id: 1, name: 'bad' }] });
    expect(result.success).toBe(false);
  });
});

// ---------------------------------------------------------------------------
// FederationGetResponseSchema factory
// ---------------------------------------------------------------------------

describe('FederationGetResponseSchema', () => {
  const ItemSchema = z.object({ id: z.string() });
  const GetSchema = FederationGetResponseSchema(ItemSchema);

  it('accepts a found item', () => {
    const result = GetSchema.safeParse({ item: { id: 'abc' } });
    expect(result.success).toBe(true);
    if (result.success) {
      expect(result.data.item).toEqual({ id: 'abc' });
    }
  });

  it('accepts null item (not found)', () => {
    const result = GetSchema.safeParse({ item: null });
    expect(result.success).toBe(true);
    if (result.success) {
      expect(result.data.item).toBeNull();
    }
  });

  it('rejects when item is missing', () => {
    const result = GetSchema.safeParse({});
    expect(result.success).toBe(false);
  });
});

// ---------------------------------------------------------------------------
// FederationCapabilitiesResponseSchema
// ---------------------------------------------------------------------------

describe('FederationCapabilitiesResponseSchema', () => {
  it('accepts a valid capabilities response', () => {
    const result = FederationCapabilitiesResponseSchema.safeParse({
      resources: ['tasks', 'notes'],
      excluded_resources: ['credentials'],
      max_rows_per_query: 500,
      supported_verbs: ['list', 'get', 'capabilities'],
    });
    expect(result.success).toBe(true);
    if (result.success) {
      expect(result.data.max_rows_per_query).toBe(500);
    }
  });

  it('accepts a response with filters field', () => {
    const result = FederationCapabilitiesResponseSchema.safeParse({
      resources: ['tasks', 'notes'],
      excluded_resources: [],
      max_rows_per_query: 100,
      supported_verbs: ['list'],
      filters: {
        tasks: { include_teams: ['team-a'], include_personal: true },
        notes: { include_personal: false },
      },
    });
    expect(result.success).toBe(true);
    if (result.success) {
      expect(result.data.filters?.['tasks']?.include_teams).toEqual(['team-a']);
    }
  });

  it('accepts a response with partial filters (only include_teams)', () => {
    const result = FederationCapabilitiesResponseSchema.safeParse({
      resources: ['tasks'],
      excluded_resources: [],
      max_rows_per_query: 50,
      supported_verbs: ['list'],
      filters: { tasks: { include_teams: ['eng'] } },
    });
    expect(result.success).toBe(true);
  });

  it('accepts a response with rate_limit (M4 full shape)', () => {
    const result = FederationCapabilitiesResponseSchema.safeParse({
      resources: ['tasks'],
      excluded_resources: [],
      max_rows_per_query: 100,
      supported_verbs: ['list'],
      rate_limit: { limit_per_minute: 60, remaining: 55, reset_at: '2026-04-23T12:00:00Z' },
    });
    expect(result.success).toBe(true);
    if (result.success) {
      expect(result.data.rate_limit?.limit_per_minute).toBe(60);
      expect(result.data.rate_limit?.remaining).toBe(55);
    }
  });

  it('accepts a response with rate_limit (M3 minimal — limit_per_minute only)', () => {
    const result = FederationCapabilitiesResponseSchema.safeParse({
      resources: ['tasks'],
      excluded_resources: [],
      max_rows_per_query: 100,
      supported_verbs: ['list'],
      rate_limit: { limit_per_minute: 120 },
    });
    expect(result.success).toBe(true);
  });

  it('accepts a response without rate_limit (field is optional)', () => {
    const result = FederationCapabilitiesResponseSchema.safeParse({
      resources: ['tasks'],
      excluded_resources: [],
      max_rows_per_query: 100,
      supported_verbs: ['list'],
    });
    expect(result.success).toBe(true);
    if (result.success) {
      expect(result.data.rate_limit).toBeUndefined();
    }
  });

  it('rejects rate_limit with non-positive limit_per_minute', () => {
    const result = FederationCapabilitiesResponseSchema.safeParse({
      resources: ['tasks'],
      excluded_resources: [],
      max_rows_per_query: 100,
      supported_verbs: ['list'],
      rate_limit: { limit_per_minute: 0 },
    });
    expect(result.success).toBe(false);
  });

  it('rejects rate_limit with invalid reset_at datetime', () => {
    const result = FederationCapabilitiesResponseSchema.safeParse({
      resources: ['tasks'],
      excluded_resources: [],
      max_rows_per_query: 100,
      supported_verbs: ['list'],
      rate_limit: { limit_per_minute: 60, reset_at: 'not-a-datetime' },
    });
    expect(result.success).toBe(false);
  });

  it('rejects supported_verbs with an invalid verb (MED-3 enum guard)', () => {
    const result = FederationCapabilitiesResponseSchema.safeParse({
      resources: ['tasks'],
      excluded_resources: [],
      max_rows_per_query: 100,
      supported_verbs: ['invalid_verb'],
    });
    expect(result.success).toBe(false);
  });

  it('rejects empty resources array', () => {
    const result = FederationCapabilitiesResponseSchema.safeParse({
      resources: [],
      excluded_resources: [],
      max_rows_per_query: 100,
      supported_verbs: ['list'],
    });
    expect(result.success).toBe(false);
  });

  it('rejects non-integer max_rows_per_query', () => {
    const result = FederationCapabilitiesResponseSchema.safeParse({
      resources: ['tasks'],
      excluded_resources: [],
      max_rows_per_query: 1.5,
      supported_verbs: ['list'],
    });
    expect(result.success).toBe(false);
  });
});

// ---------------------------------------------------------------------------
// FederationErrorEnvelopeSchema + error code exhaustiveness
// ---------------------------------------------------------------------------

describe('FederationErrorEnvelopeSchema', () => {
  it('accepts each valid error code', () => {
    for (const code of FEDERATION_ERROR_CODES) {
      const result = FederationErrorEnvelopeSchema.safeParse({
        error: { code, message: 'test' },
      });
      expect(result.success, `code ${code} should be valid`).toBe(true);
    }
  });

  it('rejects an unknown error code', () => {
    const result = FederationErrorEnvelopeSchema.safeParse({
      error: { code: 'unknown_code', message: 'test' },
    });
    expect(result.success).toBe(false);
  });

  it('accepts optional details field', () => {
    const result = FederationErrorEnvelopeSchema.safeParse({
      error: { code: 'forbidden', message: 'nope', details: { grantId: 'xyz' } },
    });
    expect(result.success).toBe(true);
  });

  it('rejects when message is missing', () => {
    const result = FederationErrorEnvelopeSchema.safeParse({ error: { code: 'not_found' } });
    expect(result.success).toBe(false);
  });
});

describe('parseFederationErrorEnvelope', () => {
  it('returns a typed envelope for valid input', () => {
    const env = parseFederationErrorEnvelope({ error: { code: 'not_found', message: 'gone' } });
    expect(env.error.code).toBe('not_found');
  });

  it('throws for invalid input', () => {
    expect(() => parseFederationErrorEnvelope({ bad: 'shape' })).toThrow();
  });
});

// ---------------------------------------------------------------------------
// FederationError exception hierarchy
// ---------------------------------------------------------------------------

describe('FederationError hierarchy', () => {
  const cases: Array<[string, FederationError]> = [
    ['unauthorized', new FederationUnauthorizedError()],
    ['forbidden', new FederationForbiddenError()],
    ['not_found', new FederationNotFoundError()],
    ['rate_limited', new FederationRateLimitedError()],
    ['scope_violation', new FederationScopeViolationError()],
    ['invalid_request', new FederationInvalidRequestError()],
    ['internal_error', new FederationInternalError()],
  ];

  it.each(cases)('code %s is an instance of FederationError', (_code, err) => {
    expect(err).toBeInstanceOf(FederationError);
    expect(err).toBeInstanceOf(Error);
  });

  it.each(cases)('code %s has correct code property', (code, err) => {
    expect(err.code).toBe(code);
  });

  it('toEnvelope serialises to wire format', () => {
    const err = new FederationForbiddenError('Access denied', { grantId: 'g1' });
    const env = err.toEnvelope();
    expect(env.error.code).toBe('forbidden');
    expect(env.error.message).toBe('Access denied');
    expect(env.error.details).toEqual({ grantId: 'g1' });
  });

  it('toEnvelope omits details when not provided', () => {
    const err = new FederationNotFoundError();
    const env = err.toEnvelope();
    expect(Object.prototype.hasOwnProperty.call(env.error, 'details')).toBe(false);
  });

  it('error codes tuple covers all subclasses (exhaustiveness check)', () => {
    // If a new subclass is added without a code, this test fails at compile time.
    const allCodes = new Set(FEDERATION_ERROR_CODES);
    for (const [code] of cases) {
      expect(allCodes.has(code as (typeof FEDERATION_ERROR_CODES)[number])).toBe(true);
    }
    // All codes are covered by at least one case
    expect(cases).toHaveLength(FEDERATION_ERROR_CODES.length);
  });
});

// ---------------------------------------------------------------------------
// Source tag + tagWithSource
// ---------------------------------------------------------------------------

describe('SourceTagSchema', () => {
  it('accepts a non-empty _source string', () => {
    expect(SourceTagSchema.safeParse({ _source: 'local' }).success).toBe(true);
    expect(SourceTagSchema.safeParse({ _source: 'mosaic.uscllc.com' }).success).toBe(true);
  });

  it('rejects empty _source string', () => {
    expect(SourceTagSchema.safeParse({ _source: '' }).success).toBe(false);
  });
});

describe('tagWithSource', () => {
  it('stamps each item with the given source', () => {
    const items = [{ id: '1' }, { id: '2' }];
    const tagged = tagWithSource(items, SOURCE_LOCAL);
    expect(tagged).toEqual([
      { id: '1', _source: 'local' },
      { id: '2', _source: 'local' },
    ]);
  });

  it('preserves original item fields', () => {
    const items = [{ id: 'x', name: 'Task', done: false }];
    const tagged = tagWithSource(items, 'mosaic.uscllc.com');
    expect(tagged[0]).toMatchObject({ id: 'x', name: 'Task', done: false });
    expect(tagged[0]?._source).toBe('mosaic.uscllc.com');
  });

  it('returns empty array for empty input', () => {
    expect(tagWithSource([], 'local')).toEqual([]);
  });

  it('round-trip: tagWithSource output passes SourceTagSchema', () => {
    const tagged = tagWithSource([{ id: '1' }], 'local');
    expect(SourceTagSchema.safeParse(tagged[0]).success).toBe(true);
  });
});
