package main

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/gavv/httpexpect"
	"github.com/julienschmidt/httprouter"
	"github.com/rs/cors"
	"github.com/satori/go.uuid"
	"gopkg.in/DATA-DOG/go-sqlmock.v1"
)

// noAuth function to write ACMETxt model to context while not preforming any validation
func noAuth(update httprouter.Handle) httprouter.Handle {
	return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
		postData := ACMETxt{}
		uname := r.Header.Get("X-Api-User")
		passwd := r.Header.Get("X-Api-Key")

		dec := json.NewDecoder(r.Body)
		_ = dec.Decode(&postData)
		// Set user info to the decoded ACMETxt object
		postData.Username, _ = uuid.FromString(uname)
		postData.Password = passwd
		// Set the ACMETxt struct to context to pull in from update function
		ctx := r.Context()
		ctx = context.WithValue(ctx, ACMETxtKey, postData)
		r = r.WithContext(ctx)
		update(w, r, p)
	}
}

func getExpect(t *testing.T, server *httptest.Server) *httpexpect.Expect {
	return httpexpect.WithConfig(httpexpect.Config{
		BaseURL:  server.URL,
		Reporter: httpexpect.NewAssertReporter(t),
		Printers: []httpexpect.Printer{
			httpexpect.NewCurlPrinter(t),
			httpexpect.NewDebugPrinter(t, true),
		},
	})
}

func setupRouter(debug bool, noauth bool) http.Handler {
	api := httprouter.New()
	var dbcfg = dbsettings{
		Engine:     "sqlite3",
		Connection: ":memory:"}
	var httpapicfg = httpapi{
		Domain:      "",
		Port:        "8080",
		TLS:         "none",
		CorsOrigins: []string{"*"},
		UseHeader:   true,
		HeaderName:  "X-Forwarded-For",
	}
	var dnscfg = DNSConfig{
		API:      httpapicfg,
		Database: dbcfg,
	}
	Config = dnscfg
	c := cors.New(cors.Options{
		AllowedOrigins:     Config.API.CorsOrigins,
		AllowedMethods:     []string{"GET", "POST"},
		OptionsPassthrough: false,
		Debug:              Config.General.Debug,
	})
	api.POST("/register", webRegisterPost)
	if noauth {
		api.POST("/update", noAuth(webUpdatePost))
	} else {
		api.POST("/update", Auth(webUpdatePost))
	}
	return c.Handler(api)
}

func TestApiRegister(t *testing.T) {
	router := setupRouter(false, false)
	server := httptest.NewServer(router)
	defer server.Close()
	e := getExpect(t, server)
	e.POST("/register").Expect().
		Status(http.StatusCreated).
		JSON().Object().
		ContainsKey("fulldomain").
		ContainsKey("subdomain").
		ContainsKey("username").
		ContainsKey("password").
		NotContainsKey("error")

	allowfrom := map[string][]interface{}{
		"allowfrom": []interface{}{"123.123.123.123/32",
			"1010.10.10.10/24",
			"invalid"},
	}

	response := e.POST("/register").
		WithJSON(allowfrom).
		Expect().
		Status(http.StatusCreated).
		JSON().Object().
		ContainsKey("fulldomain").
		ContainsKey("subdomain").
		ContainsKey("username").
		ContainsKey("password").
		ContainsKey("allowfrom").
		NotContainsKey("error")

	response.Value("allowfrom").Array().Elements("123.123.123.123/32")
}

func TestApiRegisterMalformedJSON(t *testing.T) {
	router := setupRouter(false, false)
	server := httptest.NewServer(router)
	defer server.Close()
	e := getExpect(t, server)

	malPayloads := []string{
		"{\"allowfrom': '1.1.1.1/32'}",
		"\"allowfrom\": \"1.1.1.1/32\"",
		"{\"allowfrom\": \"[1.1.1.1/32]\"",
		"\"allowfrom\": \"1.1.1.1/32\"}",
		"{allowfrom: \"1.2.3.4\"}",
		"{allowfrom: [1.2.3.4]}",
		"whatever that's not a json payload",
	}
	for _, test := range malPayloads {
		e.POST("/register").
			WithBytes([]byte(test)).
			Expect().
			Status(http.StatusBadRequest).
			JSON().Object().
			ContainsKey("error").
			NotContainsKey("subdomain").
			NotContainsKey("username")
	}
}

func TestApiRegisterWithMockDB(t *testing.T) {
	router := setupRouter(false, false)
	server := httptest.NewServer(router)
	defer server.Close()
	e := getExpect(t, server)
	oldDb := DB.GetBackend()
	db, mock, _ := sqlmock.New()
	DB.SetBackend(db)
	defer db.Close()
	mock.ExpectBegin()
	mock.ExpectPrepare("INSERT INTO records").WillReturnError(errors.New("error"))
	e.POST("/register").Expect().
		Status(http.StatusInternalServerError).
		JSON().Object().
		ContainsKey("error")
	DB.SetBackend(oldDb)
}

func TestApiUpdateWithInvalidSubdomain(t *testing.T) {
	validTxtData := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

	updateJSON := map[string]interface{}{
		"subdomain": "",
		"txt":       ""}

	router := setupRouter(false, false)
	server := httptest.NewServer(router)
	defer server.Close()
	e := getExpect(t, server)
	newUser, err := DB.Register(cidrslice{})
	if err != nil {
		t.Errorf("Could not create new user, got error [%v]", err)
	}
	// Invalid subdomain data
	updateJSON["subdomain"] = "example.com"
	updateJSON["txt"] = validTxtData
	e.POST("/update").
		WithJSON(updateJSON).
		WithHeader("X-Api-User", newUser.Username.String()).
		WithHeader("X-Api-Key", newUser.Password).
		Expect().
		Status(http.StatusUnauthorized).
		JSON().Object().
		ContainsKey("error").
		NotContainsKey("txt").
		ValueEqual("error", "forbidden")
}

func TestApiUpdateWithInvalidTxt(t *testing.T) {
	invalidTXTData := "idk m8 bbl lmao"

	updateJSON := map[string]interface{}{
		"subdomain": "",
		"txt":       ""}

	router := setupRouter(false, false)
	server := httptest.NewServer(router)
	defer server.Close()
	e := getExpect(t, server)
	newUser, err := DB.Register(cidrslice{})
	if err != nil {
		t.Errorf("Could not create new user, got error [%v]", err)
	}
	updateJSON["subdomain"] = newUser.Subdomain
	// Invalid txt data
	updateJSON["txt"] = invalidTXTData
	e.POST("/update").
		WithJSON(updateJSON).
		WithHeader("X-Api-User", newUser.Username.String()).
		WithHeader("X-Api-Key", newUser.Password).
		Expect().
		Status(http.StatusBadRequest).
		JSON().Object().
		ContainsKey("error").
		NotContainsKey("txt").
		ValueEqual("error", "bad_txt")
}

func TestApiUpdateWithoutCredentials(t *testing.T) {
	router := setupRouter(false, false)
	server := httptest.NewServer(router)
	defer server.Close()
	e := getExpect(t, server)
	e.POST("/update").Expect().
		Status(http.StatusUnauthorized).
		JSON().Object().
		ContainsKey("error").
		NotContainsKey("txt")
}

func TestApiUpdateWithCredentials(t *testing.T) {
	validTxtData := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

	updateJSON := map[string]interface{}{
		"subdomain": "",
		"txt":       ""}

	router := setupRouter(false, false)
	server := httptest.NewServer(router)
	defer server.Close()
	e := getExpect(t, server)
	newUser, err := DB.Register(cidrslice{})
	if err != nil {
		t.Errorf("Could not create new user, got error [%v]", err)
	}
	// Valid data
	updateJSON["subdomain"] = newUser.Subdomain
	updateJSON["txt"] = validTxtData
	e.POST("/update").
		WithJSON(updateJSON).
		WithHeader("X-Api-User", newUser.Username.String()).
		WithHeader("X-Api-Key", newUser.Password).
		Expect().
		Status(http.StatusOK).
		JSON().Object().
		ContainsKey("txt").
		NotContainsKey("error").
		ValueEqual("txt", validTxtData)
}

func TestApiUpdateWithCredentialsMockDB(t *testing.T) {
	validTxtData := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
	updateJSON := map[string]interface{}{
		"subdomain": "",
		"txt":       ""}

	// Valid data
	updateJSON["subdomain"] = "a097455b-52cc-4569-90c8-7a4b97c6eba8"
	updateJSON["txt"] = validTxtData

	router := setupRouter(false, true)
	server := httptest.NewServer(router)
	defer server.Close()
	e := getExpect(t, server)
	oldDb := DB.GetBackend()
	db, mock, _ := sqlmock.New()
	DB.SetBackend(db)
	defer db.Close()
	mock.ExpectBegin()
	mock.ExpectPrepare("UPDATE records").WillReturnError(errors.New("error"))
	e.POST("/update").
		WithJSON(updateJSON).
		Expect().
		Status(http.StatusInternalServerError).
		JSON().Object().
		ContainsKey("error")
	DB.SetBackend(oldDb)
}

func TestApiManyUpdateWithCredentials(t *testing.T) {
	validTxtData := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

	updateJSON := map[string]interface{}{
		"subdomain": "",
		"txt":       ""}

	router := setupRouter(true, false)
	server := httptest.NewServer(router)
	defer server.Close()
	e := getExpect(t, server)
	// User without defined CIDR masks
	newUser, err := DB.Register(cidrslice{})
	if err != nil {
		t.Errorf("Could not create new user, got error [%v]", err)
	}

	// User with defined allow from - CIDR masks, all invalid
	// (httpexpect doesn't provide a way to mock remote ip)
	newUserWithCIDR, err := DB.Register(cidrslice{"192.168.1.1/32", "invalid"})
	if err != nil {
		t.Errorf("Could not create new user with CIDR, got error [%v]", err)
	}

	// Another user with valid CIDR mask to match the httpexpect default
	newUserWithValidCIDR, err := DB.Register(cidrslice{"10.1.2.3/32", "invalid"})
	if err != nil {
		t.Errorf("Could not create new user with a valid CIDR, got error [%v]", err)
	}

	for _, test := range []struct {
		user      string
		pass      string
		subdomain string
		txt       interface{}
		status    int
	}{
		{"non-uuid-user", "tooshortpass", "non-uuid-subdomain", validTxtData, 401},
		{"a097455b-52cc-4569-90c8-7a4b97c6eba8", "tooshortpass", "bb97455b-52cc-4569-90c8-7a4b97c6eba8", validTxtData, 401},
		{"a097455b-52cc-4569-90c8-7a4b97c6eba8", "LongEnoughPassButNoUserExists___________", "bb97455b-52cc-4569-90c8-7a4b97c6eba8", validTxtData, 401},
		{newUser.Username.String(), newUser.Password, "a097455b-52cc-4569-90c8-7a4b97c6eba8", validTxtData, 401},
		{newUser.Username.String(), newUser.Password, newUser.Subdomain, "tooshortfortxt", 400},
		{newUser.Username.String(), newUser.Password, newUser.Subdomain, 1234567890, 400},
		{newUser.Username.String(), newUser.Password, newUser.Subdomain, validTxtData, 200},
		{newUserWithCIDR.Username.String(), newUserWithCIDR.Password, newUserWithCIDR.Subdomain, validTxtData, 401},
		{newUserWithValidCIDR.Username.String(), newUserWithValidCIDR.Password, newUserWithValidCIDR.Subdomain, validTxtData, 200},
		{newUser.Username.String(), "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", newUser.Subdomain, validTxtData, 401},
	} {
		updateJSON = map[string]interface{}{
			"subdomain": test.subdomain,
			"txt":       test.txt}
		e.POST("/update").
			WithJSON(updateJSON).
			WithHeader("X-Api-User", test.user).
			WithHeader("X-Api-Key", test.pass).
			WithHeader("X-Forwarded-For", "10.1.2.3").
			Expect().
			Status(test.status)
	}
}

func TestApiManyUpdateWithIpCheckHeaders(t *testing.T) {

	updateJSON := map[string]interface{}{
		"subdomain": "",
		"txt":       ""}

	router := setupRouter(false, false)
	server := httptest.NewServer(router)
	defer server.Close()
	e := getExpect(t, server)
	// Use header checks from default header (X-Forwarded-For)
	Config.API.UseHeader = true
	// User without defined CIDR masks
	newUser, err := DB.Register(cidrslice{})
	if err != nil {
		t.Errorf("Could not create new user, got error [%v]", err)
	}

	newUserWithCIDR, err := DB.Register(cidrslice{"192.168.1.2/32", "invalid"})
	if err != nil {
		t.Errorf("Could not create new user with CIDR, got error [%v]", err)
	}

	newUserWithIP6CIDR, err := DB.Register(cidrslice{"2002:c0a8::0/32"})
	if err != nil {
		t.Errorf("Could not create a new user with IP6 CIDR, got error [%v]", err)
	}

	for _, test := range []struct {
		user        ACMETxt
		headerValue string
		status      int
	}{
		{newUser, "whatever goes", 200},
		{newUser, "10.0.0.1, 1.2.3.4 ,3.4.5.6", 200},
		{newUserWithCIDR, "127.0.0.1", 401},
		{newUserWithCIDR, "10.0.0.1, 10.0.0.2, 192.168.1.3", 401},
		{newUserWithCIDR, "10.1.1.1 ,192.168.1.2, 8.8.8.8", 200},
		{newUserWithIP6CIDR, "2002:c0a8:b4dc:0d3::0", 200},
		{newUserWithIP6CIDR, "2002:c0a7:0ff::0", 401},
		{newUserWithIP6CIDR, "2002:c0a8:d3ad:b33f:c0ff:33b4:dc0d:3b4d", 200},
	} {
		updateJSON = map[string]interface{}{
			"subdomain": test.user.Subdomain,
			"txt":       "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}
		e.POST("/update").
			WithJSON(updateJSON).
			WithHeader("X-Api-User", test.user.Username.String()).
			WithHeader("X-Api-Key", test.user.Password).
			WithHeader("X-Forwarded-For", test.headerValue).
			Expect().
			Status(test.status)
	}
	Config.API.UseHeader = false
}
