package handlers

import (
	"context"
	"encoding/json"
	"go-udemy-web-1/internal/driver"
	"go-udemy-web-1/internal/models"
	"log"
	"net/http"
	"net/http/httptest"
	"net/url"
	"reflect"
	"strconv"
	"strings"
	"testing"
	"time"
)

type postData struct {
	key   string
	value string
}

// {{{ Test NewRepo

func Test_NewRepo(t *testing.T) {
	var db driver.DB
	repo := NewRepo(&app, &db)

	if reflect.TypeOf(repo).String() != "*handlers.Repository" {
		t.Errorf("repo is of type %s instead of *handlers.Repository", reflect.TypeOf(repo).String())
	}
}

// }}}
// {{{ Simple get tests

var theTests = []struct {
	name               string
	url                string
	method             string
	expectedStatusCode int
}{
	{"home", "/", "GET", http.StatusOK},
	{"about", "/about", "GET", http.StatusOK},
	{"gq", "/generals-quarters", "GET", http.StatusOK},
	{"ms", "/majors-suite", "GET", http.StatusOK},
	{"sa", "/availability", "GET", http.StatusOK},
	{"contact", "/contact", "GET", http.StatusOK},
}

func TestHandlers(t *testing.T) {
	routes := getRoutes()
	ts := httptest.NewTLSServer(routes)
	defer ts.Close()

	for _, e := range theTests {
		resp, err := ts.Client().Get(ts.URL + e.url)
		if err != nil {
			t.Log(err)
			t.Fatal(err)
		}
		if resp.StatusCode != e.expectedStatusCode {
			t.Errorf("for %s, expected %d but got %d\n", e.name, e.expectedStatusCode, resp.StatusCode)
		}
	}
}

// }}}
// {{{ Make Reservation Tests

var makeReservationTests = []struct {
	name               string
	roomID             int
	expectedStatusCode int
}{
	{"ok", 1, http.StatusOK},
	{"no session", 0, http.StatusTemporaryRedirect},
	{"non-existant room", 100, http.StatusTemporaryRedirect},
}

func TestRepository_MakeReservation(t *testing.T) {
	for _, test := range makeReservationTests {
		reservation := models.Reservation{
			RoomID: test.roomID,
			Room: models.Room{
				ID:       test.roomID,
				RoomName: "Room name",
			},
		}
		req, _ := http.NewRequest("GET", "/make-reservation", nil)
		ctx := getCtx(req)
		req = req.WithContext(ctx)
		rr := httptest.NewRecorder()
		if test.roomID == 0 {
			session.Put(ctx, "reservation", nil)
		} else {
			session.Put(ctx, "reservation", reservation)
		}
		handler := http.HandlerFunc(Repo.MakeReservation)

		handler.ServeHTTP(rr, req)

		if rr.Code != test.expectedStatusCode {
			t.Errorf("for %s, reservation handler returned response code: got %d, wanted %d\n",
				test.name, rr.Code, http.StatusOK)
		}
	}
}

// }}}
// {{{ PostMakeReservation tests

var postMakeReservationTests = []struct {
	name            string
	reservationInfo []postData

	expectedStatusCode int
}{
	{
		"ok",
		[]postData{
			{key: "first_name", value: "John"},
			{key: "last_name", value: "Smith"},
			{key: "email", value: "john@smith.com"},
			{key: "phone", value: "1234"},
			{key: "room_id", value: "1"},
			{key: "start_date", value: "2050-01-01"},
			{key: "end_date", value: "2050-01-02"},
		},
		http.StatusSeeOther,
	},
	{
		"no_session",
		[]postData{
			{key: "first_name", value: "John"},
			{key: "last_name", value: "Smith"},
			{key: "email", value: "john@smith.com"},
			{key: "phone", value: "1234"},
			{key: "room_id", value: "0"},
			{key: "start_date", value: "2050-01-01"},
			{key: "end_date", value: "2050-01-02"},
		},
		http.StatusTemporaryRedirect,
	},
	{"no_post_data", []postData{}, http.StatusTemporaryRedirect},
	{
		"missing first name",
		[]postData{
			{key: "last_name", value: "Smith"},
			{key: "email", value: "john@smith.com"},
			{key: "phone", value: "1234"},
			{key: "room_id", value: "1"},
			{key: "start_date", value: "2050-01-01"},
			{key: "end_date", value: "2050-01-02"},
		},
		http.StatusOK,
	},
	{
		"wrong first name",
		[]postData{
			{key: "first_name", value: "J"},
			{key: "last_name", value: "Smith"},
			{key: "email", value: "john@smith.com"},
			{key: "phone", value: "1234"},
			{key: "room_id", value: "1"},
			{key: "start_date", value: "2050-01-01"},
			{key: "end_date", value: "2050-01-02"},
		},
		http.StatusOK,
	},
	{
		"wrong email",
		[]postData{
			{key: "first_name", value: "John"},
			{key: "last_name", value: "Smith"},
			{key: "email", value: "john@smith"},
			{key: "phone", value: "1234"},
			{key: "room_id", value: "1"},
			{key: "start_date", value: "2050-01-01"},
			{key: "end_date", value: "2050-01-02"},
		},
		http.StatusOK,
	},
	{
		"insert reservation error",
		[]postData{
			{key: "first_name", value: "John"},
			{key: "last_name", value: "Smith"},
			{key: "email", value: "john@smith.com"},
			{key: "phone", value: "1234"},
			{key: "room_id", value: "2"},
			{key: "start_date", value: "2050-01-01"},
			{key: "end_date", value: "2050-01-02"},
		},
		http.StatusTemporaryRedirect,
	},
	{
		"insert room restriction error",
		[]postData{
			{key: "first_name", value: "John"},
			{key: "last_name", value: "Smith"},
			{key: "email", value: "john@smith.com"},
			{key: "phone", value: "1234"},
			{key: "room_id", value: "100"},
			{key: "start_date", value: "2050-01-01"},
			{key: "end_date", value: "2050-01-02"},
		},
		http.StatusTemporaryRedirect,
	},
}

func TestRepository_PostMakeReservation(t *testing.T) {
	for _, test := range postMakeReservationTests {
		roomID := 1
		reqBody := url.Values{}
		var req *http.Request
		if len(test.reservationInfo) > 0 {
			for _, element := range test.reservationInfo {
				reqBody.Add(element.key, element.value)
				if element.key == "room_id" {
					roomID, _ = strconv.Atoi(element.value)
				}
			}
			req, _ = http.NewRequest("POST", "/make-reservation", strings.NewReader(reqBody.Encode()))
		} else {
			req, _ = http.NewRequest("POST", "/make-reservation", nil)
		}
		layout := "2006-01-02"
		sd, _ := time.Parse(layout, "2050-01-01")
		ed, _ := time.Parse(layout, "2050-01-02")
		reservation := models.Reservation{
			RoomID:    roomID,
			StartDate: sd,
			EndDate:   ed,
		}

		ctx := getCtx(req)
		req = req.WithContext(ctx)
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

		rr := httptest.NewRecorder()
		if roomID == 0 {
			session.Put(ctx, "reservation", nil)
		} else {
			session.Put(ctx, "reservation", reservation)
		}

		handler := http.HandlerFunc(Repo.PostMakeReservation)
		handler.ServeHTTP(rr, req)

		if rr.Code != test.expectedStatusCode {
			t.Errorf("for %s, reservation handler returned response code: got %d, wanted %d\n",
				test.name, rr.Code, test.expectedStatusCode)
		}
	}
}

// }}}
// {{{ Test AvailabilityJSON

var availabilityJSONTests = []struct {
	name               string
	queryInfo          []postData
	expectedStatusJSON jsonResponse
}{
	{"ok", []postData{
		{key: "start", value: "2050-01-01"},
		{key: "end", value: "2050-01-02"},
		{key: "room_id", value: "1"},
	}, jsonResponse{
		OK:        true,
		Message:   "",
		StartDate: "2050-01-01",
		EndDate:   "2050-01-02",
		RoomID:    "1",
	}},
	{"not available", []postData{
		{key: "start", value: "2050-01-01"},
		{key: "end", value: "2050-01-02"},
		{key: "room_id", value: "2"},
	}, jsonResponse{
		OK:        false,
		Message:   "",
		StartDate: "2050-01-01",
		EndDate:   "2050-01-02",
		RoomID:    "2",
	}},
	{"wrong start date", []postData{
		{key: "start", value: "2050-01"},
		{key: "end", value: "2050-01-02"},
		{key: "room_id", value: "1"},
	}, jsonResponse{
		OK:      false,
		Message: "Wrong startDate",
	}},
	{"wrong end date", []postData{
		{key: "start", value: "2050-01-01"},
		{key: "end", value: "wrong"},
		{key: "room_id", value: "1"},
	}, jsonResponse{
		OK:      false,
		Message: "Wrong endDate",
	}},
	{"wrong room id", []postData{
		{key: "start", value: "2050-01-01"},
		{key: "end", value: "2050-01-02"},
		{key: "room_id", value: "x"},
	}, jsonResponse{
		OK:      false,
		Message: "Wrong roomID",
	}},
	{"not available", []postData{
		{key: "start", value: "2050-01-01"},
		{key: "end", value: "2050-01-02"},
		{key: "room_id", value: "100"},
	}, jsonResponse{
		OK:      false,
		Message: "Error connecting to database",
	}},
	{"no form", []postData{}, jsonResponse{
		OK:      false,
		Message: "Internal server error",
	}},
}

func Test_AvailabilityJSON(t *testing.T) {
	for _, test := range availabilityJSONTests {
		var req *http.Request
		reqBody := url.Values{}
		if len(test.queryInfo) > 0 {
			for _, element := range test.queryInfo {
				reqBody.Add(element.key, element.value)
			}

			req, _ = http.NewRequest("POST", "/make-reservation", strings.NewReader(reqBody.Encode()))
		} else {
			req, _ = http.NewRequest("POST", "/make-reservation", nil)
		}
		ctx := getCtx(req)
		req = req.WithContext(ctx)
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

		rr := httptest.NewRecorder()

		handler := http.HandlerFunc(Repo.AvailabilityJSON)
		handler.ServeHTTP(rr, req)

		if rr.Code != http.StatusOK {
			t.Errorf("for %s, reservation handler returned response code: got %d, wanted %d\n",
				test.name, rr.Code, http.StatusOK)
		}

		var j jsonResponse
		err := json.Unmarshal(rr.Body.Bytes(), &j)
		if err != nil {
			t.Errorf("for %s, failed to parse json", test.name)
		}
		if j != test.expectedStatusJSON {
			expected, _ := json.MarshalIndent(test.expectedStatusJSON, "", "    ")
			t.Errorf("for %s, returned json is wrong, expected: %s, returned: %s",
				test.name, expected, rr.Body.String())
		}

	}
}

// }}}
// {{{ Test ReservationSummary

var reservationSummaryTests = []struct {
	name               string
	haveSession        bool
	expectedStatusCode int
}{
	{"ok", true, http.StatusOK},
	{"nok", false, http.StatusTemporaryRedirect},
}

func Test_ReservationSummary(t *testing.T) {
	for _, test := range reservationSummaryTests {
		req, _ := http.NewRequest("GET", "/reservation-summary", nil)
		ctx := getCtx(req)
		req = req.WithContext(ctx)

		rr := httptest.NewRecorder()

		layout := "2006-01-02"
		sd, _ := time.Parse(layout, "2050-01-01")
		ed, _ := time.Parse(layout, "2050-01-02")
		reservation := models.Reservation{
			StartDate: sd,
			EndDate:   ed,
			FirstName: "John",
			LastName:  "Smith",
			Email:     "john@smith.com",
			RoomID:    1,
		}

		if test.haveSession {
			session.Put(req.Context(), "reservation", reservation)
		}

		handler := http.HandlerFunc(Repo.ReservationSummary)
		handler.ServeHTTP(rr, req)

		if rr.Code != test.expectedStatusCode {
			t.Errorf("for %s, reservation handler returned response code: got %d, wanted %d\n",
				test.name, rr.Code, test.expectedStatusCode)
		}
	}
}

// }}}
// {{{ Test PostAvailability

var postAvailabilityTests = []struct {
	name               string
	queryInfo          []postData
	expectedStatusCode int
}{
	{"ok", []postData{
		{key: "start", value: "2050-01-01"},
		{key: "end", value: "2050-01-02"},
	}, http.StatusOK},
	{"database error", []postData{
		{key: "start", value: "2050-01-03"},
		{key: "end", value: "2050-01-04"},
	}, http.StatusTemporaryRedirect},
	{"no availability", []postData{
		{key: "start", value: "2050-01-05"},
		{key: "end", value: "2050-01-06"},
	}, http.StatusSeeOther},
	{"wrong start date", []postData{
		{key: "start", value: "2050-05"},
		{key: "end", value: "2050-01-06"},
	}, http.StatusTemporaryRedirect},
	{"wrong end date", []postData{
		{key: "start", value: "2050-01-05"},
		{key: "end", value: "01-06"},
	}, http.StatusTemporaryRedirect},
	{"wrong end date", []postData{}, http.StatusTemporaryRedirect},
}

func Test_PostAvailability(t *testing.T) {
	for _, test := range postAvailabilityTests {
		var req *http.Request
		if len(test.queryInfo) > 0 {
			reqBody := url.Values{}
			for _, element := range test.queryInfo {
				reqBody.Add(element.key, element.value)
			}

			req, _ = http.NewRequest("POST", "/availability", strings.NewReader(reqBody.Encode()))
		} else {
			req, _ = http.NewRequest("POST", "/availability", nil)
		}
		ctx := getCtx(req)
		req = req.WithContext(ctx)
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

		rr := httptest.NewRecorder()

		handler := http.HandlerFunc(Repo.PostAvailability)
		handler.ServeHTTP(rr, req)

		if rr.Code != test.expectedStatusCode {
			t.Errorf("for %s, reservation handler returned response code: got %d, wanted %d\n",
				test.name, rr.Code, test.expectedStatusCode)
		}
	}
}

// }}}
// {{{ Test ChooseRoom

var chooseRoomTests = []struct {
	name               string
	request            string
	haveSession        bool
	expectedStatusCode int
}{
	{"ok", "/choose-room/1", true, http.StatusSeeOther},
	{"wrong room id", "/choose-room/1wrong", true, http.StatusTemporaryRedirect},
	{"no session", "/choose-room/1", false, http.StatusTemporaryRedirect},
}

func Test_ChooseRoom(t *testing.T) {
	for _, test := range chooseRoomTests {
		req, _ := http.NewRequest("GET", test.request, nil)
		ctx := getCtx(req)
		req = req.WithContext(ctx)

		layout := "2006-01-02"
		sd, _ := time.Parse(layout, "2050-01-01")
		ed, _ := time.Parse(layout, "2050-01-02")
		reservation := models.Reservation{
			StartDate: sd,
			EndDate:   ed,
		}

		if test.haveSession {
			session.Put(req.Context(), "reservation", reservation)
		}
		rr := httptest.NewRecorder()

		handler := http.HandlerFunc(Repo.ChooseRoom)
		handler.ServeHTTP(rr, req)

		if rr.Code != test.expectedStatusCode {
			t.Errorf("for %s, reservation handler returned response code: got %d, wanted %d\n",
				test.name, rr.Code, test.expectedStatusCode)
		}
	}
}

// }}}
// {{{ Test BookRoom

var bookRoomTests = []struct {
	name               string
	queryInfo          []postData
	expectedStatusCode int
}{
	{"ok", []postData{
		{key: "id", value: "1"},
		{key: "s", value: "2050-01-01"},
		{key: "e", value: "2050-01-02"},
	}, http.StatusSeeOther},
	{"wrong start date", []postData{
		{key: "id", value: "1"},
		{key: "s", value: "20-01-01"},
		{key: "e", value: "2050-01-02"},
	}, http.StatusTemporaryRedirect},
	{"wrong end date", []postData{
		{key: "id", value: "1"},
		{key: "s", value: "2050-01-01"},
		{key: "e", value: "2050-0-02"},
	}, http.StatusTemporaryRedirect},
	{"wrong room id", []postData{
		{key: "id", value: "w"},
		{key: "s", value: "2050-01-01"},
		{key: "e", value: "2050-01-02"},
	}, http.StatusTemporaryRedirect},
}

func Test_BookRoom(t *testing.T) {
	for _, test := range bookRoomTests {
		request := "/book-room?"
		for _, element := range test.queryInfo {
			request += element.key + "=" + element.value + "&"
		}
		request = request[:len(request)-1]
		req, _ := http.NewRequest("GET", request, nil)
		ctx := getCtx(req)
		req = req.WithContext(ctx)

		rr := httptest.NewRecorder()

		handler := http.HandlerFunc(Repo.BookRoom)
		handler.ServeHTTP(rr, req)

		if rr.Code != test.expectedStatusCode {
			t.Errorf("for %s, reservation handler returned response code: got %d, wanted %d\n",
				test.name, rr.Code, test.expectedStatusCode)
		}
	}
}

// }}}
// {{{ Test Helpers

func getCtx(req *http.Request) context.Context {
	ctx, err := session.Load(req.Context(), req.Header.Get("X-Session"))
	if err != nil {
		log.Println(err)
	}

	return ctx
}

// }}}
