import { describe, test, expect } from 'bun:test';
import {
  PathSchema,
  RequestIdSchema,
  CursorSchema,
  Sha256HexSchema,
  FileEntrySchema,
  FileKindSchema,
  ListPageSchema,
  ListArgsSchema,
  StatArgsSchema,
  MkdirArgsSchema,
  DeleteArgsSchema,
  MoveArgsSchema,
  ReadArgsSchema,
  ReadResultSchema,
  WriteArgsSchema,
  GetThumbnailArgsSchema,
  GetThumbnailResultSchema,
  CustomArgsSchema,
  RpcRequestSchema,
  RpcResponseSchema,
  RpcErrorSchema,
  RpcCancelSchema,
  FileErrorCodeSchema,
  FileErrorPayloadSchema,
} from '../../src/index.js';

describe('PathSchema', () => {
  test('accepts absolute paths', () => {
    expect(PathSchema.parse('/foo')).toBe('/foo');
    expect(PathSchema.parse('/foo/bar/baz.txt')).toBe('/foo/bar/baz.txt');
    expect(PathSchema.parse('/')).toBe('/');
  });
  test('rejects relative paths', () => {
    expect(() => PathSchema.parse('foo')).toThrow();
    expect(() => PathSchema.parse('./foo')).toThrow();
  });
  test('rejects NUL/CR/LF/DEL/backslash', () => {
    expect(() => PathSchema.parse('/foo\x00bar')).toThrow();
    expect(() => PathSchema.parse('/foo\r\n')).toThrow();
    expect(() => PathSchema.parse('/foo\x7f')).toThrow();
    expect(() => PathSchema.parse('/foo\\bar')).toThrow();
  });
  test('rejects empty + over-length', () => {
    expect(() => PathSchema.parse('')).toThrow();
    expect(() => PathSchema.parse('/' + 'a'.repeat(4096))).toThrow();
  });
  test('accepts UTF-8 in filenames', () => {
    expect(PathSchema.parse('/Документы/файл.txt')).toBe('/Документы/файл.txt');
    expect(PathSchema.parse('/πρόβλημα/αρχείο')).toBe('/πρόβλημα/αρχείο');
  });
});

describe('RequestIdSchema', () => {
  test('accepts 22-char base64url', () => {
    expect(RequestIdSchema.parse('AbCdEfGhIjKlMnOpQrStUv')).toBe('AbCdEfGhIjKlMnOpQrStUv');
  });
  test('rejects wrong length', () => {
    expect(() => RequestIdSchema.parse('AbCd')).toThrow();
    expect(() => RequestIdSchema.parse('AbCdEfGhIjKlMnOpQrStUvWxYz')).toThrow();
  });
  test('rejects non-base64url chars', () => {
    expect(() => RequestIdSchema.parse('AbCd/EfGh+IjKlMnOpQrST')).toThrow();
    expect(() => RequestIdSchema.parse('AbCd=EfGhIjKlMnOpQrSTUV')).toThrow();
  });
});

describe('CursorSchema', () => {
  test('accepts up to 2048 chars', () => {
    expect(CursorSchema.parse('a')).toBe('a');
    expect(CursorSchema.parse('x'.repeat(2048)).length).toBe(2048);
  });
  test('rejects empty + over-length', () => {
    expect(() => CursorSchema.parse('')).toThrow();
    expect(() => CursorSchema.parse('x'.repeat(2049))).toThrow();
  });
});

describe('Sha256HexSchema', () => {
  test('accepts 64 hex chars', () => {
    const h = '0'.repeat(64);
    expect(Sha256HexSchema.parse(h)).toBe(h);
  });
  test('rejects wrong length / non-hex', () => {
    expect(() => Sha256HexSchema.parse('0'.repeat(63))).toThrow();
    expect(() => Sha256HexSchema.parse('0'.repeat(65))).toThrow();
    expect(() => Sha256HexSchema.parse('g'.repeat(64))).toThrow();
    expect(() => Sha256HexSchema.parse('A'.repeat(64))).toThrow(); // uppercase rejected
  });
});

describe('FileKind / FileEntry', () => {
  test('FileKind accepts file/dir', () => {
    expect(FileKindSchema.parse('file')).toBe('file');
    expect(FileKindSchema.parse('dir')).toBe('dir');
    expect(() => FileKindSchema.parse('symlink')).toThrow();
  });
  test('FileEntry roundtrip', () => {
    const e = {
      name: 'foo.txt',
      kind: 'file' as const,
      size: 1024,
      mtime: 1730000000000,
      contentType: 'text/plain',
    };
    const parsed = FileEntrySchema.parse(e);
    expect(parsed.name).toBe('foo.txt');
    expect(parsed.metadata).toEqual({}); // default
  });
  test('FileEntry rejects path separators in name', () => {
    expect(() =>
      FileEntrySchema.parse({ name: 'foo/bar', kind: 'file', size: 0, mtime: 0 }),
    ).toThrow();
    expect(() =>
      FileEntrySchema.parse({ name: 'foo\\bar', kind: 'file', size: 0, mtime: 0 }),
    ).toThrow();
  });
  test('FileEntry rejects negative size', () => {
    expect(() =>
      FileEntrySchema.parse({ name: 'a', kind: 'file', size: -1, mtime: 0 }),
    ).toThrow();
  });
  test('FileEntry passes through metadata', () => {
    const parsed = FileEntrySchema.parse({
      name: 'mod.jar',
      kind: 'file',
      size: 100,
      mtime: 0,
      metadata: { modrinthId: 'abc', version: '1.0' },
    });
    expect(parsed.metadata).toEqual({ modrinthId: 'abc', version: '1.0' });
  });
});

describe('ListPage', () => {
  test('hasMore + nextCursor when more pages', () => {
    const p = ListPageSchema.parse({
      entries: [],
      hasMore: true,
      nextCursor: 'abc',
    });
    expect(p.hasMore).toBe(true);
    expect(p.nextCursor).toBe('abc');
  });
  test('no nextCursor when hasMore false', () => {
    const p = ListPageSchema.parse({ entries: [], hasMore: false });
    expect(p.nextCursor).toBeUndefined();
  });
});

describe('ListArgs', () => {
  test('defaults pageSize to 100', () => {
    const a = ListArgsSchema.parse({ path: '/foo' });
    expect(a.pageSize).toBe(100);
  });
  test('rejects pageSize > 1000', () => {
    expect(() => ListArgsSchema.parse({ path: '/foo', pageSize: 1001 })).toThrow();
  });
  test('rejects pageSize < 1', () => {
    expect(() => ListArgsSchema.parse({ path: '/foo', pageSize: 0 })).toThrow();
  });
});

describe('StatArgs', () => {
  test('requires path', () => {
    expect(() => StatArgsSchema.parse({})).toThrow();
    expect(StatArgsSchema.parse({ path: '/foo' }).path).toBe('/foo');
  });
});

describe('Mkdir/Delete/Move args', () => {
  test('Mkdir defaults recursive=false', () => {
    expect(MkdirArgsSchema.parse({ path: '/a' }).recursive).toBe(false);
  });
  test('Delete defaults recursive=false', () => {
    expect(DeleteArgsSchema.parse({ path: '/a' }).recursive).toBe(false);
  });
  test('Move defaults overwrite=false', () => {
    expect(
      MoveArgsSchema.parse({ src: '/a', dst: '/b' }).overwrite,
    ).toBe(false);
  });
});

describe('ReadArgs / ReadResult', () => {
  test('Read accepts optional range', () => {
    const a = ReadArgsSchema.parse({ path: '/a', range: { start: 0, end: 100 } });
    expect(a.range).toEqual({ start: 0, end: 100 });
  });
  test('ReadResult inline', () => {
    const r = ReadResultSchema.parse({
      kind: 'inline',
      bytesB64: 'YQ==',
      size: 1,
      sha256: 'a'.repeat(64),
    });
    expect(r.kind).toBe('inline');
  });
  test('ReadResult streams', () => {
    const r = ReadResultSchema.parse({
      kind: 'streams',
      streamId: 'sid-123',
      size: 1024,
      sha256: 'b'.repeat(64),
    });
    expect(r.kind).toBe('streams');
  });
  test('ReadResult rejects unknown kind', () => {
    expect(() =>
      ReadResultSchema.parse({ kind: 'magic', bytesB64: 'YQ==' }),
    ).toThrow();
  });
});

describe('WriteArgs', () => {
  test('inline shape', () => {
    const w = WriteArgsSchema.parse({
      kind: 'inline',
      path: '/foo',
      bytesB64: 'YQ==',
    });
    expect(w.kind).toBe('inline');
    if (w.kind === 'inline') expect(w.overwrite).toBe(false);
  });
  test('streams shape', () => {
    const w = WriteArgsSchema.parse({
      kind: 'streams',
      path: '/foo',
      size: 1000,
      writeId: 'AbCdEfGhIjKlMnOpQrStUv',
    });
    if (w.kind === 'streams') expect(w.size).toBe(1000);
  });
});

describe('GetThumbnailArgs / Result', () => {
  test('size must be enum 64/128/256/512', () => {
    expect(() => GetThumbnailArgsSchema.parse({ path: '/a', size: 100 })).toThrow();
    expect(GetThumbnailArgsSchema.parse({ path: '/a', size: 64 }).size).toBe(64);
  });
  test('format defaults to png', () => {
    const a = GetThumbnailArgsSchema.parse({ path: '/a', size: 128 });
    expect(a.format).toBe('png');
  });
  test('Result requires width/height/sha256', () => {
    const r = GetThumbnailResultSchema.parse({
      bytesB64: 'YQ==',
      format: 'png',
      width: 128,
      height: 128,
      sha256: 'c'.repeat(64),
    });
    expect(r.width).toBe(128);
  });
});

describe('CustomArgs', () => {
  test('accepts dotted op names', () => {
    expect(
      CustomArgsSchema.parse({ name: 'dispatch.deploy-mod', payload: {} }).name,
    ).toBe('dispatch.deploy-mod');
  });
  test('rejects names with spaces or slashes', () => {
    expect(() => CustomArgsSchema.parse({ name: 'foo bar', payload: {} })).toThrow();
    expect(() => CustomArgsSchema.parse({ name: 'foo/bar', payload: {} })).toThrow();
  });
});

describe('RPC envelopes', () => {
  test('RpcRequest valid', () => {
    const env = RpcRequestSchema.parse({
      kind: 'shade.fs.list/v1',
      id: 'AbCdEfGhIjKlMnOpQrStUv',
      args: { path: '/' },
      sig: 'sig',
      signedAt: 1730000000000,
    });
    expect(env.kind).toBe('shade.fs.list/v1');
  });
  test('RpcRequest rejects malformed kind', () => {
    expect(() =>
      RpcRequestSchema.parse({
        kind: 'shade.fs.list',
        id: 'AbCdEfGhIjKlMnOpQrStUv',
        args: {},
        sig: 's',
        signedAt: 1,
      }),
    ).toThrow();
  });
  test('RpcResponse expects .response suffix', () => {
    const env = RpcResponseSchema.parse({
      kind: 'shade.fs.list/v1.response',
      id: 'AbCdEfGhIjKlMnOpQrStUv',
      result: { entries: [], hasMore: false },
    });
    expect(env.id.length).toBe(22);
  });
  test('RpcError fixed kind', () => {
    const env = RpcErrorSchema.parse({
      kind: 'shade.fs.error/v1',
      id: 'AbCdEfGhIjKlMnOpQrStUv',
      error: { code: 'NOT_FOUND', message: 'gone' },
    });
    expect(env.error.code).toBe('NOT_FOUND');
  });
  test('RpcCancel fixed kind', () => {
    const env = RpcCancelSchema.parse({
      kind: 'shade.fs.cancel/v1',
      id: 'AbCdEfGhIjKlMnOpQrStUv',
      reason: 'user-cancel',
    });
    expect(env.reason).toBe('user-cancel');
  });
});

describe('FileError envelope', () => {
  test('all known codes parse', () => {
    const codes: ReadonlyArray<unknown> = [
      'NOT_FOUND',
      'PERMISSION_DENIED',
      'CONFLICT',
      'QUOTA_EXCEEDED',
      'RATE_LIMIT',
      'PATH_VALIDATION',
      'FINGERPRINT_REQUIRED',
      'OPERATION_TIMEOUT',
      'IDEMPOTENCY_CONFLICT',
      'CANCELLED',
      'INTERNAL',
      'NOT_IMPLEMENTED',
      'CUSTOM_OP_REJECTED',
      'INVALID_SIGNATURE',
      'INVALID_ARGS',
    ];
    for (const c of codes) expect(FileErrorCodeSchema.parse(c)).toBe(c);
  });
  test('rejects unknown code', () => {
    expect(() => FileErrorCodeSchema.parse('NOT_REAL')).toThrow();
  });
  test('FileErrorPayload includes optional fields', () => {
    const p = FileErrorPayloadSchema.parse({
      code: 'RATE_LIMIT',
      message: 'too fast',
      retryAfterMs: 1000,
    });
    expect(p.retryAfterMs).toBe(1000);
  });
});
