import { Route, RouteOptions } from '.';
import { expect } from '../../test';

describe('url.Route', () => {
  describe('constructor', () => {
    it('default values', () => {
      const route = Route.create('   /foo/:id   ');
      expect(route.method).to.eql('GET');
      expect(route.path).to.eql('/foo/:id');
      expect(route.toString()).to.eql('/foo/:id');
      expect(route.title).to.eql(undefined);
      expect(route.docs).to.eql(undefined);
      expect(route.description).to.eql(undefined);
      expect(route.meta).to.eql({});
    });

    it('given values', () => {
      const route = Route.create('/foo/:id', {
        title: 'My Title',
        docs: '/docs',
        description: 'Does...',
        meta: { foo: 123 },
      });
      expect(route.title).to.eql('My Title');
      expect(route.docs).to.eql('/docs');
      expect(route.description).to.eql('Does...');
      expect(route.meta).to.eql({ foo: 123 });
    });

    it('sets REST method', () => {
      const route = Route.create('/foo/:id', { method: 'POST' });
      expect(route.method).to.eql('POST');
    });

    it('symbols', () => {
      const route = Route.create('/foo/:id', {
        schema: { params: 'IFoo', query: 'IBam', data: 'IBoo' },
      });
      expect(route.schema.params).to.eql('IFoo');
      expect(route.schema.query).to.eql('IBam');
      expect(route.schema.data).to.eql('IBoo');
    });

    it('title/docs', () => {
      const route = Route.create('/foo/:id', {
        title: 'MyRoute',
        docs: '/docs/my-route',
      });
      expect(route.title).to.eql('MyRoute');
      expect(route.docs).to.eql('/docs/my-route');
    });
  });

  describe('clone', () => {
    it('clones with same values', () => {
      const route = Route.create('/foo/:id');
      const clone = route.clone();
      expect(route).to.not.equal(clone);
    });

    it('clones with new value', () => {
      const route = Route.create('/foo/:id', {});
      const clone = route.clone({ path: '/bar' });
      expect(route).to.not.equal(clone);
      expect(clone.path).to.eql('/bar');
    });
  });

  describe('toObject', () => {
    it('with default values', () => {
      const route = Route.create('/');
      const obj = route.toObject();
      expect(obj.path).to.eql('/');
      expect(obj.method).to.eql('GET');
      expect(obj.tokens).to.eql(['/']);
      expect(obj.schema).to.eql({});
    });

    it('with specific values', () => {
      const route = Route.create('/foo/:id', {
        method: 'POST',
        schema: { params: 'IFoo' },
      });
      const obj = route.toObject();
      expect(obj.path).to.eql('/foo/:id');
      expect(obj.method).to.eql('POST');
      expect(obj.tokens.length).to.eql(2);
      expect(obj.tokens[0]).to.eql('/foo');
      expect(obj.tokens[1]).to.eql({
        name: 'id',
        prefix: '/',
        delimiter: '/',
        optional: false,
        repeat: false,
        partial: false,
      });
      expect(obj.schema).to.eql({ params: 'IFoo' });
    });
  });

  describe('isMatch', () => {
    const test = (
      url: string | undefined,
      result: boolean,
      options?: Partial<RouteOptions>,
    ) => {
      const route = Route.create('/foo/:id', { ...options });
      expect(route.isMatch(url)).to.eql(result);
    };
    it('is a match', () => {
      test('/foo/123', true);
      test('/foo/abc', true);
      test('/foo/abc?q=123', true);
      test('/foo/abc/', true, { strict: false });
      test('/Foo/bar', true, { caseSensitive: false }); // Case sentitive (no match)
    });

    it('is not a match', () => {
      test(undefined, false);
      test('', false);
      test('/', false);
      test('/foobar', false);
      test('/foo/abc/', false); // Strict (by default)
      test('/Foo/bar', false); // Case sentitive (no match)
    });

    it('multi-part', () => {
      const route = Route.create('/o/:name/:path*');
      const path = '/o/acme/foo/bar';
      expect(route.isMatch(path)).to.eql(true);
      expect(route.params(path)).to.eql({ name: 'acme', path: 'foo/bar' });
    });
  });

  describe('params (strongly typed)', () => {
    type IParams = {
      org: string;
      id: number | string;
    };
    const route = Route.create<IParams>('/foo/:org/:id');

    it('no url (undefined}', () => {
      const res = route.params();
      expect(res).to.eql({});
    });

    it(':id as string', () => {
      const res = route.params('/foo/ibm/hello');
      expect(res.org).to.eql('ibm');
      expect(res.id).to.eql('hello');
    });

    it(':id as number', () => {
      const res = route.params('/foo/ibm/123');
      expect(res.org).to.eql('ibm');
      expect(res.id).to.eql(123);
    });

    it(':id as boolean', () => {
      const res = route.params('/foo/ibm/TruE');
      expect(res.org).to.eql('ibm');
      expect(res.id).to.eql(true);
    });

    it('has escaped colon (:) in route', () => {
      const route = Route.create<{ id: number }>('/foo::id');
      const res = route.params('/foo:123');
      expect(res).to.eql({ id: 123 });
    });
  });

  describe('params', () => {
    it('wildcard (*)', () => {
      const route = Route.create<{ bar: string }>('/foo/:bar*');
      const res = route.params('/foo/bar/bax');
      expect(res.bar).to.eql('bar/bax');
    });

    it('removes query-string', () => {
      const route = Route.create<{ id: number }>('/foo/:id?q=:srch');
      const res = route.params('/foo/123?q=search');
      expect(res.id).to.eql(123);
    });
  });

  describe('query', () => {
    type IParams = { id: number | string };
    type IQuery = { q: string; force?: boolean };
    const route = Route.create<IParams, IQuery>('/foo/:id');

    it('as string', () => {
      const query = route.query('/foo/123?q=abc');
      expect(query.q).to.eql('abc');
    });

    it('as number', () => {
      const query = route.query('/foo/123?q=456');
      expect(query.q).to.eql(456);
    });

    it('as boolean', () => {
      const query = route.query('/foo/123?q=truE');
      expect(query.q).to.eql(true);
    });

    it('flag', () => {
      const test = (url: string, result?: boolean) => {
        const query = route.query(url);
        expect(query.force).to.eql(result);
      };
      test('/foo?force', true);
      test('/foo?force=true', true);
      test('/foo?force=false', false);
      test('/foo', undefined);
    });
  });

  describe('url', () => {
    type IParams = { id: number | string };
    type IQuery = { q: string; force?: boolean; f?: boolean };
    const URL = '/foo/123?q=hello&force';
    const route = Route.create<IParams, IQuery>('/foo/:id', {});
    const url = route.url(URL);

    it('creates URL', () => {
      expect(url.route).to.eql(route);
      expect(url.path).to.eql(URL);
      expect(url.toString()).to.eql(URL);
      expect(url.params).to.eql({ id: 123 });
      expect(url.query).to.eql({ q: 'hello', force: true });
    });

    it('url with origin (domain)', () => {
      const complete = `http://localhost:3000/foo/123?q=hello&force`;
      const res1 = url.toString({ origin: 'http://localhost:3000' });
      const res2 = url.toString({ origin: 'http://localhost:3000/' });
      const res3 = url.toString({ origin: 'http://localhost:3000///' });
      expect(res1).to.eql(complete);
      expect(res2).to.eql(complete);
      expect(res3).to.eql(complete);
    });

    it('hasFlag', () => {
      expect(url.hasFlag('force')).to.eql(true);
      expect(url.hasFlag('f')).to.eql(false);
      expect(url.hasFlag(['f', 'force'])).to.eql(true);
      expect(url.hasFlag()).to.eql(false);
      expect(url.hasFlag('bob')).to.eql(false);
    });
  });

  describe('toUrl', () => {
    type IQuery = { q: string; force?: boolean; f?: boolean };
    type IParams = {
      org: string;
      id: number | string;
    };
    const route = Route.create<IParams, IQuery>('/foo/:org/:id');

    it('no params (error)', () => {
      const fn = () => {
        route.toUrl({ params: {} as any });
      };
      expect(fn).to.throw(/Expected "org"/);
    });

    it('params only', () => {
      const url = route.toUrl({
        params: { org: 'acme', id: 123 },
      }).s;
      expect(url).to.eql('/foo/acme/123');
    });

    it('query as [:param]', () => {
      const route = Route.create('/manifest?path=:path&markdown');
      const url = route.toUrl({ params: { path: '/my/path' } });
      expect(url.toString()).to.eql('/manifest?path=%2Fmy%2Fpath&markdown');
      expect(url.query).to.eql({ path: '/my/path', markdown: true });
      expect(url.params).to.eql({ path: '%2Fmy%2Fpath' });
    });

    it('query as [:param] as well as {object}', () => {
      const route = Route.create('/manifest?path=:path&markdown');
      const url = route.toUrl({
        params: { path: '**' },
        query: { s: 'find' },
      });
      expect(url.toString()).to.eql('/manifest?path=**&markdown&s=find');
      expect(url.query).to.eql({ path: '**', markdown: true, s: 'find' });
    });

    it('encoded', () => {
      const org = 'café verona';
      const url = route.toUrl({
        params: { org, id: 123 },
      }).s;
      expect(url).to.eql('/foo/caf%C3%A9%20verona/123');
      expect(decodeURI(url)).to.eql('/foo/café verona/123');
    });

    it('params and query-string', () => {
      const url = route
        .toUrl({
          params: { org: 'acme', id: 123 },
          query: { q: 'search' },
        })
        .toString();
      expect(url).to.eql('/foo/acme/123?q=search');
    });

    it('multi part query-string', () => {
      const url = route
        .toUrl({
          params: { org: 'acme', id: 123 },
          query: { q: 'hello', f: true },
        })
        .toString();
      expect(url).to.eql('/foo/acme/123?q=hello&f=true');
    });

    it('query-string with no value (undeined)', () => {
      const url = route
        .toUrl({
          params: { org: 'acme', id: 123 },
          query: { q: 'hello', f: undefined },
        })
        .toString();
      expect(url).to.eql('/foo/acme/123?q=hello');
    });

    it('encoded query-string', () => {
      const url = route
        .toUrl({
          params: { org: 'acme', id: 123 },
          query: { q: 'foo bar' },
        })
        .toString();
      expect(url).to.eql('/foo/acme/123?q=foo%20bar');
    });

    it('has no origin by default', () => {
      const url = route.toUrl({ params: { org: 'acme', id: 123 } });
      expect(url.origin).to.eql(undefined);
      expect(url.toString()).to.eql('/foo/acme/123');
    });

    it('sets a default origin', () => {
      const url = route.toUrl({
        params: { org: 'acme', id: 123 },
        origin: 'http://localhost:8080//',
      });
      expect(url.origin).to.eql('http://localhost:8080');
      expect(url.toString()).to.eql('http://localhost:8080/foo/acme/123');

      // NB: Override with parameter.
      expect(url.toString({ origin: 'localhost:3000' })).to.eql(
        'localhost:3000/foo/acme/123',
      );
    });
  });

  describe('walk/map/find', () => {
    const TREE = {
      id: 123,
      foo: Route.create('/foo'),
      bar: {
        baz: Route.create('/foo/bar'),
        label: 'Hello',
        zoo: {
          child: Route.create('/deep'),
        },
      },
    };

    it('walks a tree', () => {
      let routes: Route[] = [];
      Route.walk(TREE, route => (routes = [...routes, route]));
      expect(routes.length).to.eql(3);
      expect(routes[0].path).to.eql('/foo');
      expect(routes[1].path).to.eql('/foo/bar');
      expect(routes[2].path).to.eql('/deep');
    });

    it('walks a tree and stops before end', () => {
      let count = 0;
      Route.walk(TREE, (route, e) => {
        if (count >= 1) {
          e.stop();
        }
        count++;
      });
      expect(count).to.eql(2);
    });

    it('maps a tree', () => {
      const res = Route.map(TREE, route => route.path);
      expect(res).to.eql(['/foo', '/foo/bar', '/deep']);
    });

    it('finds a route', () => {
      const res1 = Route.find(TREE, r => r.path.startsWith('/foo'));
      const res2 = Route.find(TREE, r => r.path.startsWith('/deep'));
      const res3 = Route.find(TREE, r => r.path.startsWith('/__NOT_EXIST__'));
      expect(res1 && res1.path).to.eql('/foo');
      expect(res2 && res2.path).to.eql('/deep');
      expect(res3).to.eql(undefined);
    });
  });
});
