package api

import (
	"net/http"
	"net/url"
	"testing"

	"github.com/grafana/grafana/pkg/api/response"
	"github.com/grafana/grafana/pkg/infra/log"
	contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
	"github.com/grafana/grafana/pkg/web"
	"github.com/stretchr/testify/require"
)

func TestHooks(t *testing.T) {
	t.Run("Hooks", func(t *testing.T) {
		t.Run("yields handlers for paths with hooks", func(t *testing.T) {
			hooks := NewHooks(log.NewNopLogger())
			invoked := false
			hook := func(*contextmodel.ReqContext) response.Response { invoked = true; return nil }

			hooks.Set("GET", "/some/path", hook)
			reqURL, _ := url.Parse("http://domain.test/some/path")
			handler, ok := hooks.Get("GET", reqURL)

			require.True(t, ok, "hooks did not contain a matching hook for path")
			require.False(t, invoked, "hook was invoked earlier than expected")
			handler(nil)
			require.True(t, invoked, "the hook returned by get() was not invoked as expected")
		})

		t.Run("yields no handlers for paths without hooks", func(t *testing.T) {
			hooks := NewHooks(log.NewNopLogger())
			hook := func(*contextmodel.ReqContext) response.Response { return nil }

			hooks.Set("GET", "/some/path", hook)
			reqURL, _ := url.Parse("http://domain.test/does/not/match")
			handler, ok := hooks.Get("GET", reqURL)

			require.False(t, ok, "hooks returned a hook when we expected it not to")
			require.Nil(t, handler)
		})

		t.Run("hooks do not match routes with additional subpaths", func(t *testing.T) {
			hooks := NewHooks(log.NewNopLogger())
			hook := func(*contextmodel.ReqContext) response.Response { return nil }

			hooks.Set("GET", "/some/path", hook)
			reqURL, _ := url.Parse("http://domain.test/some/path/with/more")
			handler, ok := hooks.Get("GET", reqURL)

			require.False(t, ok, "hooks returned a hook when we expected it not to")
			require.Nil(t, handler)
		})

		t.Run("hooks do not match requests with the wrong HTTP method", func(t *testing.T) {
			hooks := NewHooks(log.NewNopLogger())
			hook := func(*contextmodel.ReqContext) response.Response { return nil }

			hooks.Set("POST", "/some/path", hook)
			reqURL, _ := url.Parse("http://domain.test/some/path")
			handler, ok := hooks.Get("GET", reqURL)

			require.False(t, ok, "hooks returned a hook when we expected it not to")
			require.Nil(t, handler)
		})

		t.Run("hooks match routes with query parameters", func(t *testing.T) {
			hooks := NewHooks(log.NewNopLogger())
			invoked := false
			hook := func(*contextmodel.ReqContext) response.Response { invoked = true; return nil }

			hooks.Set("GET", "/some/path", hook)
			reqURL, _ := url.Parse("http://domain.test/some/path?query=param")
			handler, ok := hooks.Get("GET", reqURL)

			require.True(t, ok, "hooks did not contain a matching hook for path")
			require.False(t, invoked, "hook was invoked earlier than expected")
			handler(nil)
			require.True(t, invoked, "the hook returned by get() was not invoked as expected")
		})

		t.Run("hooks match routes with path variables", func(t *testing.T) {
			hooks := NewHooks(log.NewNopLogger())
			invoked := false
			hook := func(*contextmodel.ReqContext) response.Response { invoked = true; return nil }

			hooks.Set("GET", "/some/{value}", hook)
			reqURL, _ := url.Parse("http://domain.test/some/123")
			handler, ok := hooks.Get("GET", reqURL)

			require.True(t, ok, "hooks did not contain a matching hook for path")
			require.False(t, invoked, "hook was invoked earlier than expected")
			handler(nil)
			require.True(t, invoked, "the hook returned by get() was not invoked as expected")
		})
	})

	t.Run("Wrap", func(t *testing.T) {
		t.Run("invokes hooks if one is defined", func(t *testing.T) {
			defaultInvoked := false
			defaultHandler := func(*contextmodel.ReqContext) response.Response { defaultInvoked = true; return nil }
			hookInvoked := false
			hookHandler := func(*contextmodel.ReqContext) response.Response { hookInvoked = true; return nil }
			hooks := NewHooks(log.NewNopLogger())
			hooks.Set("GET", "/some/path", hookHandler)

			composed := hooks.Wrap(defaultHandler)
			req := createReqForTests("GET", "http://domain.test/some/path")
			composed(req)

			require.True(t, hookInvoked, "hook was expected to be invoked, but it was not")
			require.False(t, defaultInvoked, "default handler was invoked, but it should not have been")
		})

		t.Run("does not invoke hooks if path has none defined", func(t *testing.T) {
			defaultInvoked := false
			defaultHandler := func(*contextmodel.ReqContext) response.Response { defaultInvoked = true; return nil }
			hookInvoked := false
			hookHandler := func(*contextmodel.ReqContext) response.Response { hookInvoked = true; return nil }
			hooks := NewHooks(log.NewNopLogger())
			hooks.Set("GET", "/some/path", hookHandler)

			composed := hooks.Wrap(defaultHandler)
			req := createReqForTests("GET", "http://domain.test/does/not/match")
			composed(req)

			require.False(t, hookInvoked, "hook was invoked, but it should not have been")
			require.True(t, defaultInvoked, "default handler was expected to be invoked, but it was not")
		})
	})
}

func createReqForTests(method, setupURL string) *contextmodel.ReqContext {
	reqURL, _ := url.Parse(setupURL)
	return &contextmodel.ReqContext{
		Context: &web.Context{
			Req: &http.Request{
				Method: method,
				URL:    reqURL,
			},
		},
	}
}