package act.route;

/*-
 * #%L
 * ACT Framework
 * %%
 * Copyright (C) 2014 - 2017 ActFramework
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */

import static act.route.RouteSource.*;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.osgl.http.H.Method.GET;

import act.controller.ParamNames;
import act.handler.RequestHandler;
import act.handler.builtin.AlwaysNotFound;
import act.handler.builtin.FileGetter;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.Mockito;
import org.osgl.$;
import org.osgl.Osgl;
import org.osgl.exception.ConfigurationException;
import org.osgl.exception.NotAppliedException;
import org.osgl.http.H;
import org.osgl.util.C;

import java.io.File;

public class RouterTest extends RouterTestBase {
    private RequestHandler staticDirHandler;

    @BeforeClass
    public static void classInit() {
        UrlPath.testClassInit();
    }

    @Override
    protected void buildRouteMapping(Router router) {
    }

    @Before
    public void prepare() {
        staticDirHandler = Mockito.mock(FileGetter.class);
        when(staticDirHandler.supportPartialPath()).thenReturn(true);
    }

    @Test
    public void testMappingAdded() {
        no(router.isMapped(GET, "/foo"));
        router.addMapping(GET, "/foo", "Foo.bar");
        yes(router.isMapped(GET, "/foo"));
    }

    // #295 Exception using underscore in a URL path variable name
    @Test
    public void testGH295() {
        router.addMapping(GET, "/foo/{var_name}", "Foo.bar");
        yes(router.isMapped(GET, "/foo/{var_name}"));
    }

    // Different URL variable name caused duplicate routes not been reported
    @Test(expected = DuplicateRouteMappingException.class)
    public void testGH561() {
        router.addMapping(GET, "/foo/{a}", "Foo.bar", RouteSource.ACTION_ANNOTATION);
        router.addMapping(GET, "/foo/{b}", "Foo.foo", RouteSource.ACTION_ANNOTATION);
    }

    @Test
    public void testGH561_0() {
        router.addMapping(GET, "/foo/{<[a-z]+>a}", "Foo.bar", RouteSource.ACTION_ANNOTATION);
        router.addMapping(GET, "/foo/{<[0-9]+>b}", "Foo.foo", RouteSource.ACTION_ANNOTATION);
    }

    @Test(expected = DuplicateRouteMappingException.class)
    public void testGH561_extended() {
        router.addMapping(GET, "/foo/{a}/b", "Foo.bar", RouteSource.ACTION_ANNOTATION);
        router.addMapping(GET, "/foo/{b}/b", "Foo.foo", RouteSource.ACTION_ANNOTATION);
    }

    @Test(expected = DuplicateRouteMappingException.class)
    public void testGH561_extended2() {
        router.addMapping(GET, "/foo/{<[0-9]{4}>a}/b", "Foo.bar", RouteSource.ACTION_ANNOTATION);
        router.addMapping(GET, "/foo/{<[0-9]{4}>y}/b", "Foo.foo", RouteSource.ACTION_ANNOTATION);
    }

    @Test
    public void testGH561_extended3() {
        router.addMapping(GET, "/foo/{<[0-9]{5}>a}/b", "Foo.bar", RouteSource.ACTION_ANNOTATION);
        router.addMapping(GET, "/foo/{<[0-9]{4}>y}/b", "Foo.foo", RouteSource.ACTION_ANNOTATION);
    }

    @Test
    public void searchRoot() {
        router.addMapping(GET, "/", controller);
        router.getInvoker(GET, "/", ctx).handle(ctx);
        controllerInvoked();
    }

    @Test
    public void searchBadUrl() {
        RequestHandler handler = router.getInvoker(GET, "/nonexists", ctx);
        same(handler, AlwaysNotFound.INSTANCE);
    }

    @Test
    public void searchStaticUrl() {
        router.addMapping(H.Method.POST, "/foo/bar", controller);
        router.getInvoker(H.Method.POST, "/foo/bar", ctx).handle(ctx);
        controllerInvoked();
    }

    @Test
    public void searchDynamicUrl() {
        router.addMapping(GET, "/svc/{<[0-9]{4}>id}", controller);
        H.Request req = Mockito.mock(H.Request.class);
        when(ctx.req()).thenReturn(req);
        when(req.path()).thenReturn("/svc/1234");
        router.getInvoker(GET, "/svc/1234/", ctx).handle(ctx);
        controllerInvoked();
        verify(ctx).urlPathParam("id", "1234");
    }

    @Test
    public void searchDynamicUrl2() {
        router.addMapping(GET, "/svc/{<[0-9]{4}>id}-{name}", controller);
        H.Request req = Mockito.mock(H.Request.class);
        when(ctx.req()).thenReturn(req);
        when(req.path()).thenReturn("/svc/1234-abc");
        router.getInvoker(GET, "/svc/1234-abc/", ctx).handle(ctx);
        controllerInvoked();
        verify(ctx).urlPathParam("id", "1234");
        verify(ctx).urlPathParam("name", "abc");
    }

    @Test
    public void searchDynamicUrl3() {
        router.addMapping(GET, "/svc/{<[0-9]{4}>id}-{name}", controller);
        router.addMapping(GET, "/svc/{<[0-9]{4}>sid}-{sname}/obj", controller);
        router.addMapping(GET, "/Persons/Joe/Parents;generations={gen}", controller);
        router.addMapping(GET, "/place/{latitude};{longitude}", controller);

        H.Request req = Mockito.mock(H.Request.class);
        when(ctx.req()).thenReturn(req);

        when(req.path()).thenReturn("/svc/1234-abc");
        when(ctx.urlPath()).thenReturn(UrlPath.of("/svc/1234-abc"));
        router.getInvoker(GET, "/svc/1234-abc", ctx).handle(ctx);
        verify(ctx).urlPathParam("id", "1234");
        verify(ctx).urlPathParam("name", "abc");

        when(req.path()).thenReturn("/svc/1234-abc/obj");
        when(ctx.urlPath()).thenReturn(UrlPath.of("/svc/1234-abc/obj"));
        router.getInvoker(GET, "/svc/1234-abc/obj", ctx).handle(ctx);
        verify(ctx).urlPathParam("sid", "1234");
        verify(ctx).urlPathParam("sname", "abc");

        when(req.path()).thenReturn("/Persons/Joe/Parents;generations=147");
        when(ctx.urlPath()).thenReturn(UrlPath.of("/Persons/Joe/Parents;generations=147"));
        router.getInvoker(GET, "/Persons/Joe/Parents;generations=147", ctx).handle(ctx);
        verify(ctx).urlPathParam("gen", "147");

        when(req.path()).thenReturn("/place/39.87381;-86.1399");
        when(ctx.urlPath()).thenReturn(UrlPath.of("/place/39.87381;-86.1399"));
        router.getInvoker(GET, "/place/39.87381;-86.1399", ctx).handle(ctx);
        verify(ctx).urlPathParam("latitude", "39.87381");
        verify(ctx).urlPathParam("longitude", "-86.1399");
    }

    @Test
    public void regExtStyleA() {
        _regExtTests("n:[0-9]+");
    }

    @Test
    public void regExtStyleB2() {
        _regExtTests("{n<[0-9]+>}");
    }

    private void _regExtTests(String pattern) {
        router.addMapping(GET, "/int/" + pattern, controller);
        H.Request req = Mockito.mock(H.Request.class);
        when(ctx.req()).thenReturn(req);

        when(req.path()).thenReturn("/int/33");
        RequestHandler handler = router.getInvoker(GET, "/int/33", ctx);
        same(controller, handler);

        verify(ctx).urlPathParam("n", "33");
    }

    @Test
    public void searchPathEndsWithIgnoreNotation() {
        router.addMapping(GET, "/foo/bar/...", controller);
        router.addMapping(GET, "/svc/{id}/...", controller);

        H.Request req = Mockito.mock(H.Request.class);
        when(ctx.req()).thenReturn(req);

        when(req.path()).thenReturn("/foo/bar/something-should-be-ignored");
        RequestHandler handler = router.getInvoker(GET, "/foo/bar/something-should-be-ignored", ctx);
        same(controller, handler);

        when(req.path()).thenReturn("/svc/123/another-thing-should-be-ignored/and-whatever/else");
        router.getInvoker(GET, "/svc/123/another-thing-should-be-ignored/and-whatever/else", ctx).handle(ctx);
        verify(ctx).urlPathParam("id", "123");
    }

    @Test(expected = ConfigurationException.class)
    public void routingConfiguredWithEndStarConflict1() {
        router.addMapping(GET, "/foo/bar/...", controller);
        router.addMapping(GET, "/foo/bar/xyz", controller);
    }

    @Test(expected = ConfigurationException.class)
    public void routingConfiguredWithEndStarConflict2() {
        router.addMapping(GET, "/foo/bar/xyz", controller);
        router.addMapping(GET, "/foo/bar/...", controller);
    }

    @Test
    public void searchPartialUrl() {
        router.addMapping(GET, "/public", staticDirHandler);
        router.getInvoker(GET, "/public/foo/bar.txt", ctx).handle(ctx);
        verify(staticDirHandler).handle(ctx);
        verify(ctx).param(ParamNames.PATH, "/foo/bar.txt");
    }

    @Test
    public void routeWithStaticDir() {
        router.addMapping(GET, "/public", "file:/public");
        RequestHandler handler = router.getInvoker(GET, "/public/foo/bar.txt", ctx);
        yes(handler instanceof FileGetter);
        yes(handler.supportPartialPath());
        eq(new File(BASE, "/public"), fieldVal(handler, "base"));
    }

    @Test
    public void overrideExistingRouting() {
        routeWithStaticDir();
        router.addMapping(GET, "/public", "file:/private");
        RequestHandler handler = router.getInvoker(GET, "/public/foo/bar.txt", ctx);
        yes(handler instanceof FileGetter);
        yes(handler.supportPartialPath());
        eq(new File(BASE, "/private"), fieldVal(handler, "base"));
    }

    @Test
    public void doNotOverrideExistingRouting() {
        routeWithStaticDir();
        router.addMapping(GET, "/public", "file:/private", ACTION_ANNOTATION);
        RequestHandler handler = router.getInvoker(GET, "/public/foo/bar.txt", ctx);
        yes(handler instanceof FileGetter);
        yes(handler.supportPartialPath());
        eq(new File(BASE, "/public"), fieldVal(handler, "base"));
    }

    @Test
    public void senseControllerMethodWithoutControllerPackage() {
        router.addMapping(GET, "/foo", "Controller.foo");
        no(router.isActionMethod("foo.controller.Controller", "foo"));
        yes(router.isActionMethod("Controller", "foo"));

        router.addMapping(GET, "/bar", "com.newcontroller.Controller.bar");
        yes(router.isActionMethod("com.newcontroller.Controller", "bar"));
    }

    @Test
    public void itShallNotOverwriteRouteMappingWithSameRouteSource() {
        for (RouteSource source : RouteSource.values()) {
            router = new Router(controllerLookup, app);
            router.addMapping(GET, "/foo", "Controller.foo", source);
            try {
                router.addMapping(GET, "/foo", "Foo.bar", source);
                if (source != ROUTE_TABLE && source != ADMIN_OVERWRITE) {
                    fail("expected DuplicateRouteMappingException");
                }
            } catch (DuplicateRouteMappingException e) {
                // good
            }
        }
    }

    @Test
    public void testAddingTwoRoutesWithSameDynamicPart() {
        router.addMapping(GET, "/foo/{id}", controller);
        router.addMapping(GET, "/foo/{id}/bar", controller);
        eq(controller, router.getInvoker(GET, "/foo/123", ctx));
        eq(controller, router.getInvoker(GET, "/foo/something/bar", ctx));
    }

    @Test(expected = DuplicateRouteMappingException.class)
    public void itShallNotAllowAddingHandlersToSameRouteEndingWithDynamicPart() {
        router.addMapping(GET, "/foo/{id}", "Controller.foo", ACTION_ANNOTATION);
        router.addMapping(GET, "/foo/{id}", "Foo.bar", ACTION_ANNOTATION);
    }

    @Test
    public void testReverseRoute() {
        router.addMapping(GET, "/foo/bar", "pkg.Foo.bar");
        eq(router.reverseRoute("pkg.Foo.bar"), "/foo/bar");
        eq(router.reverseRoute("pkg.Foo.bar", C.<String, Object>map("foo", "bar")), "/foo/bar?foo=bar");
    }

    @Test
    public void testReverseRouteWithPathVar() {
        router.addMapping(GET, "/foo/{foo}", "pkg.Foo.foo");
        router.addMapping(GET, "/foo/{fooId}/bar/{barId}", "pkg.Foo.bar");
        eq(router.reverseRoute("pkg.Foo.bar"), "/foo/-/bar/-");
        eq(router.reverseRoute("pkg.Foo.bar", C.<String, Object>map("fooId", 1, "barId", 3)), "/foo/1/bar/3");
        eq(router.reverseRoute("pkg.Foo.foo", C.<String, Object>map("foo", 1)), "/foo/1");
    }

    @Test
    public void testReverseRouteWithPathVar2() {
        router.addMapping(GET, "/foo/{fooId}-{barId}", "pkg.Foo.bar");
        router.addMapping(GET, "/foo/{foo}.htm", "pkg.Foo.foo");
        //eq(router.reverseRoute("pkg.Foo.bar"), "/foo/---");
        eq(router.reverseRoute("pkg.Foo.bar", C.<String, Object>map("fooId", 1, "barId", 3)), "/foo/1-3");
        eq(router.reverseRoute("pkg.Foo.foo", C.<String, Object>map("foo", 1)), "/foo/1.htm");
    }

    @Test
    public void testInferFullActionPath() {
        final String currentActionPath = "com.my.comp.proj_a.controller.MyController.login";
        $.Func0<String> provider = new $.Func0<String>() {
            @Override
            public String apply() throws NotAppliedException, Osgl.Break {
                return currentActionPath;
            }
        };
        eq("com.my.comp.proj_a.controller.MyController.home", Router.inferFullActionPath("home", provider));
        eq("com.my.comp.proj_a.controller.YourController.home", Router.inferFullActionPath("YourController.home", provider));
        eq("pkg.YourController.home", Router.inferFullActionPath("pkg.YourController.home", provider));
    }

    @Test
    public void ghIssue121() {
        router.addMapping(GET, "/abc/134/foo/{a}", "pkg.Foo.any", ACTION_ANNOTATION);
        router.addMapping(GET, "/abc/134/foo/bar", "pkg.Foo.bar", ACTION_ANNOTATION);
        router.addMapping(GET, "/abc/134/foo/zee", "pkg.Foo.zee", ACTION_ANNOTATION);
        eq("/abc/134/foo/abc", router.reverseRoute("pkg.Foo.any", C.<String, Object>map("a", "abc")));
        eq("/abc/134/foo/bar", router.reverseRoute("pkg.Foo.bar"));
        eq("/abc/134/foo/zee", router.reverseRoute("pkg.Foo.zee"));
    }

}
