import {
  expandUriTemplate,
  extractTemplateParams,
  isUriTemplate,
  matchUriTemplate,
  parseUriTemplate,
} from './uri-template';

describe('URI Template Utils', () => {
  describe('parseUriTemplate', () => {
    it('should parse simple template with one param', () => {
      const result = parseUriTemplate('users/{userId}');

      expect(result.paramNames).toEqual(['userId']);
      expect(result.pattern.test('users/123')).toBe(true);
      expect(result.pattern.test('users/')).toBe(false);
    });

    it('should parse template with multiple params', () => {
      const result = parseUriTemplate('users/{userId}/posts/{postId}');

      expect(result.paramNames).toEqual(['userId', 'postId']);
      expect(result.pattern.test('users/123/posts/456')).toBe(true);
      expect(result.pattern.test('users/123/posts')).toBe(false);
    });

    it('should escape regex special characters', () => {
      const result = parseUriTemplate('file:///{path}');

      expect(result.paramNames).toEqual(['path']);
      expect(result.pattern.test('file:///documents')).toBe(true);
      expect(result.pattern.test('file://documents')).toBe(false);
    });

    it('should return empty paramNames for no-param template', () => {
      const result = parseUriTemplate('static://resource');

      expect(result.paramNames).toEqual([]);
      expect(result.pattern.test('static://resource')).toBe(true);
      expect(result.pattern.test('static://other')).toBe(false);
    });

    it('should handle template with special regex chars like dots', () => {
      const result = parseUriTemplate('api.example.com/{resource}');

      expect(result.paramNames).toEqual(['resource']);
      expect(result.pattern.test('api.example.com/users')).toBe(true);
      expect(result.pattern.test('apiXexampleXcom/users')).toBe(false);
    });

    it('should throw for templates exceeding max length', () => {
      const longTemplate = 'a'.repeat(1001);
      expect(() => parseUriTemplate(longTemplate)).toThrow('too long');
    });

    it('should throw for templates with too many parameters', () => {
      const manyParams = Array.from({ length: 51 }, (_, i) => `{p${i}}`).join('/');
      expect(() => parseUriTemplate(manyParams)).toThrow('too many parameters');
    });

    it('should strip the + operator from reserved expansion param names', () => {
      const result = parseUriTemplate('skill://{+skillPath}/SKILL.md');

      expect(result.paramNames).toEqual(['skillPath']);
      expect(result.pattern.test('skill://acme/SKILL.md')).toBe(true);
      // Reserved expansion can span multiple path segments
      expect(result.pattern.test('skill://acme/billing/refunds/SKILL.md')).toBe(true);
    });

    it('should support multiple reserved-expansion params in one template', () => {
      const result = parseUriTemplate('skill://{+skillPath}/{+filePath}');

      expect(result.paramNames).toEqual(['skillPath', 'filePath']);
      expect(result.pattern.test('skill://docs/refs/getting-started.md')).toBe(true);
    });
  });

  describe('matchUriTemplate', () => {
    it('should match URI and extract single param', () => {
      const params = matchUriTemplate('users/{userId}', 'users/123');

      expect(params).toEqual({ userId: '123' });
    });

    it('should match URI and extract multiple params', () => {
      const params = matchUriTemplate('users/{userId}/posts/{postId}', 'users/abc/posts/xyz');

      expect(params).toEqual({ userId: 'abc', postId: 'xyz' });
    });

    it('should return null for non-matching URI', () => {
      const params = matchUriTemplate('users/{userId}', 'posts/123');

      expect(params).toBeNull();
    });

    it('should decode URL-encoded param values', () => {
      const params = matchUriTemplate('files/{name}', 'files/hello%20world');

      expect(params).toEqual({ name: 'hello world' });
    });

    it('should not match slash in param value', () => {
      const params = matchUriTemplate('files/{name}', 'files/path/to/file');

      expect(params).toBeNull();
    });

    it('should handle special characters in URI', () => {
      const params = matchUriTemplate('file:///{path}', 'file:///documents');

      expect(params).toEqual({ path: 'documents' });
    });

    it('should return empty object for static template that matches', () => {
      const params = matchUriTemplate('static://resource', 'static://resource');

      expect(params).toEqual({});
    });

    it('should match multi-segment values with reserved expansion', () => {
      const params = matchUriTemplate('skill://{+skillPath}/SKILL.md', 'skill://acme/billing/refunds/SKILL.md');

      expect(params).toEqual({ skillPath: 'acme/billing/refunds' });
    });

    it('should split between two reserved-expansion params at the first slash', () => {
      const params = matchUriTemplate(
        'skill://{+skillPath}/{+filePath}',
        'skill://docs-skill/references/getting-started.md',
      );

      // Lazy match: first reserved variable claims the smallest viable
      // prefix, leaving the rest to the second variable.
      expect(params).toEqual({ skillPath: 'docs-skill', filePath: 'references/getting-started.md' });
    });

    it('should still reject unrelated URIs for reserved-expansion templates', () => {
      const params = matchUriTemplate('skill://{+skillPath}/SKILL.md', 'other://acme/SKILL.md');

      expect(params).toBeNull();
    });
  });

  describe('expandUriTemplate', () => {
    it('should expand template with params', () => {
      const result = expandUriTemplate('users/{userId}', { userId: '123' });

      expect(result).toBe('users/123');
    });

    it('should expand template with multiple params', () => {
      const result = expandUriTemplate('users/{userId}/posts/{postId}', {
        userId: 'abc',
        postId: 'xyz',
      });

      expect(result).toBe('users/abc/posts/xyz');
    });

    it('should URL-encode param values', () => {
      const result = expandUriTemplate('files/{name}', { name: 'hello world' });

      expect(result).toBe('files/hello%20world');
    });

    it('should URL-encode slashes in param values', () => {
      const result = expandUriTemplate('files/{path}', { path: 'path/to/file' });

      expect(result).toBe('files/path%2Fto%2Ffile');
    });

    it('should throw for missing required param', () => {
      expect(() => {
        expandUriTemplate('users/{userId}/posts/{postId}', { userId: '123' });
      }).toThrow("Missing parameter 'postId'");
    });

    it('should handle static template with no params', () => {
      const result = expandUriTemplate('static://resource', {});

      expect(result).toBe('static://resource');
    });

    it('should preserve slashes in reserved-expansion params', () => {
      const result = expandUriTemplate('skill://{+skillPath}/SKILL.md', { skillPath: 'acme/billing' });

      expect(result).toBe('skill://acme/billing/SKILL.md');
    });

    it('should percent-encode chars outside the reserved+unreserved set', () => {
      const result = expandUriTemplate('skill://{+skillPath}/SKILL.md', { skillPath: 'spaces and unicode ✨' });

      expect(result).toBe('skill://spaces%20and%20unicode%20%E2%9C%A8/SKILL.md');
    });

    it('should normalize pre-encoded triplets in reserved-expansion inputs', () => {
      // Inputs are treated as raw values: any `%xx` triplet is decoded
      // first, then re-encoded only if outside the unreserved+reserved
      // sets. So `foo%2Fbar` decodes to `foo/bar`, and `/` is reserved,
      // leaving `foo/bar`. This matches the RFC 6570 §3.2.3 contract.
      const result = expandUriTemplate('skill://{+skillPath}/SKILL.md', { skillPath: 'foo%2Fbar' });

      expect(result).toBe('skill://foo/bar/SKILL.md');
    });

    it('should percent-encode chars that need encoding in reserved expansion', () => {
      const result = expandUriTemplate('skill://{+skillPath}/SKILL.md', { skillPath: 'pre encoded' });

      expect(result).toBe('skill://pre%20encoded/SKILL.md');
    });
  });

  describe('extractTemplateParams', () => {
    it('should extract param names from template', () => {
      const params = extractTemplateParams('users/{userId}/posts/{postId}');

      expect(params).toEqual(['userId', 'postId']);
    });

    it('should return empty array for no params', () => {
      const params = extractTemplateParams('static://resource');

      expect(params).toEqual([]);
    });

    it('should preserve param order', () => {
      const params = extractTemplateParams('{a}/{b}/{c}');

      expect(params).toEqual(['a', 'b', 'c']);
    });

    it('should strip the + operator from reserved-expansion param names', () => {
      const params = extractTemplateParams('skill://{+skillPath}/{+filePath}');

      expect(params).toEqual(['skillPath', 'filePath']);
    });
  });

  describe('isUriTemplate', () => {
    it('should return true for template with {param}', () => {
      expect(isUriTemplate('users/{userId}')).toBe(true);
    });

    it('should return true for template with multiple params', () => {
      expect(isUriTemplate('users/{userId}/posts/{postId}')).toBe(true);
    });

    it('should return false for static URI', () => {
      expect(isUriTemplate('static://resource')).toBe(false);
    });

    it('should return false for empty string', () => {
      expect(isUriTemplate('')).toBe(false);
    });

    it('should return false for malformed braces', () => {
      expect(isUriTemplate('users/{}')).toBe(false);
      expect(isUriTemplate('users/{')).toBe(false);
      expect(isUriTemplate('users/}')).toBe(false);
    });
  });
});
