package server

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"errors"
	"log"
	"net/http"
	"strings"
	"testing"
	"time"

	"fmt"
	"github.com/fnproject/fn/api/datastore"
	"github.com/fnproject/fn/api/logs"
	"github.com/fnproject/fn/api/models"
	"github.com/fnproject/fn/api/mqs"
	"github.com/gin-gonic/gin"
	"github.com/sirupsen/logrus"
)

func setLogBuffer() *bytes.Buffer {
	var buf bytes.Buffer
	buf.WriteByte('\n')
	logrus.SetOutput(&buf)
	gin.DefaultErrorWriter = &buf
	gin.DefaultWriter = &buf
	log.SetOutput(&buf)
	return &buf
}

func TestAppCreate(t *testing.T) {
	buf := setLogBuffer()
	defer func() {
		if t.Failed() {
			t.Log(buf.String())
		}
	}()

	for i, test := range []struct {
		mock          models.Datastore
		logDB         models.LogStore
		path          string
		body          string
		expectedCode  int
		expectedError error
	}{
		// errors
		{datastore.NewMock(), logs.NewMock(), "/v2/apps", ``, http.StatusBadRequest, models.ErrInvalidJSON},
		{datastore.NewMock(), logs.NewMock(), "/v2/apps", `{}`, http.StatusBadRequest, models.ErrMissingName},
		{datastore.NewMock(), logs.NewMock(), "/v2/apps", `{"name": "app", "id":"badId"}`, http.StatusBadRequest, models.ErrAppIDProvided},
		{datastore.NewMock(), logs.NewMock(), "/v2/apps", `{ "name": "" }`, http.StatusBadRequest, models.ErrMissingName},
		{datastore.NewMock(), logs.NewMock(), "/v2/apps", `{"name": "1234567890123456789012345678901" }`, http.StatusBadRequest, models.ErrAppsTooLongName},
		{datastore.NewMock(), logs.NewMock(), "/v2/apps", `{ "name": "&&%@!#$#@$" }`, http.StatusBadRequest, models.ErrAppsInvalidName},
		{datastore.NewMock(), logs.NewMock(), "/v2/apps", `{ "name": "app", "annotations" : { "":"val" }}`, http.StatusBadRequest, models.ErrInvalidAnnotationKey},
		{datastore.NewMock(), logs.NewMock(), "/v2/apps", `{"name": "app", "annotations" : { "key":"" }}`, http.StatusBadRequest, models.ErrInvalidAnnotationValue},
		{datastore.NewMock(), logs.NewMock(), "/v2/apps", `{ "name": "app", "syslog_url":"yo"}`, http.StatusBadRequest, errors.New(`invalid syslog url: "yo"`)},
		{datastore.NewMock(), logs.NewMock(), "/v2/apps", `{"name": "app", "syslog_url":"yo://sup.com:1"}`, http.StatusBadRequest, errors.New(`invalid syslog url: "yo://sup.com:1" invalid scheme, only [tcp, udp, unix, unixgram, tcp+tls] are supported`)},
		// success
		{datastore.NewMock(), logs.NewMock(), "/v2/apps", `{ "name": "teste"  }`, http.StatusOK, nil},
		{datastore.NewMock(), logs.NewMock(), "/v2/apps", `{  "name": "teste" , "annotations": {"k1":"v1", "k2":[]}}`, http.StatusOK, nil},
		{datastore.NewMock(), logs.NewMock(), "/v2/apps", `{"name": "teste", "syslog_url":"tcp://example.com:443" } `, http.StatusOK, nil},
		{datastore.NewMockInit([]*models.App{&models.App{ID: "appid", Name: "teste"}}), logs.NewMock(), "/v2/apps", `{ "name": "teste"  }`, http.StatusConflict, models.ErrAppsAlreadyExists},
	} {
		rnr, cancel := testRunner(t)
		srv := testServer(test.mock, &mqs.Mock{}, test.logDB, rnr, ServerTypeFull)
		router := srv.Router

		body := bytes.NewBuffer([]byte(test.body))
		_, rec := routerRequest(t, router, "POST", test.path, body)

		if rec.Code != test.expectedCode {
			t.Errorf("Test %d: Expected status code to be %d but was %d",
				i, test.expectedCode, rec.Code)
		}

		if test.expectedError != nil {
			resp := getErrorResponse(t, rec)

			if !strings.Contains(resp.Message, test.expectedError.Error()) {
				t.Errorf("Test %d: Expected error message to have `%s` but got `%s`",
					i, test.expectedError.Error(), resp.Message)
			}
		}

		if test.expectedCode == http.StatusOK {
			var app models.App
			err := json.NewDecoder(rec.Body).Decode(&app)
			if err != nil {
				t.Log(buf.String())
				t.Errorf("Test %d: error decoding body for 'ok' json, it was a lie: %v", i, err)
			}

			// IsZero() doesn't really work, this ensures it's not unset as long as we're not in 1970
			if time.Time(app.CreatedAt).Before(time.Now().Add(-1 * time.Hour)) {
				t.Log(buf.String())
				t.Errorf("Test %d: expected created_at to be set on app, it wasn't: %s", i, app.CreatedAt)
			}
			if !(time.Time(app.CreatedAt)).Equal(time.Time(app.UpdatedAt)) {
				t.Log(buf.String())
				t.Errorf("Test %d: expected updated_at to be set and same as created at, it wasn't: %s %s", i, app.CreatedAt, app.UpdatedAt)
			}
		}

		cancel()
	}
}

func TestAppDelete(t *testing.T) {
	buf := setLogBuffer()
	defer func() {
		if t.Failed() {
			t.Log(buf.String())
		}
	}()

	app := &models.App{
		Name: "myapp",
		ID:   "appId",
	}
	ds := datastore.NewMockInit([]*models.App{app})
	for i, test := range []struct {
		ds            models.Datastore
		logDB         models.LogStore
		path          string
		body          string
		expectedCode  int
		expectedError error
	}{
		{datastore.NewMock(), logs.NewMock(), "/v2/apps/myapp", "", http.StatusNotFound, nil},
		{ds, logs.NewMock(), "/v2/apps/appId", "", http.StatusNoContent, nil},
	} {
		rnr, cancel := testRunner(t)
		srv := testServer(test.ds, &mqs.Mock{}, test.logDB, rnr, ServerTypeFull)

		_, rec := routerRequest(t, srv.Router, "DELETE", test.path, nil)

		if rec.Code != test.expectedCode {
			t.Errorf("Test %d: Expected status code to be %d but was %d",
				i, test.expectedCode, rec.Code)
		}

		if test.expectedError != nil {
			resp := getErrorResponse(t, rec)

			if !strings.Contains(resp.Message, test.expectedError.Error()) {
				t.Errorf("Test %d: Expected error message to have `%s`",
					i, test.expectedError.Error())
			}
		}
		cancel()
	}
}

func TestAppList(t *testing.T) {
	buf := setLogBuffer()
	defer func() {
		if t.Failed() {
			t.Log(buf.String())
		}
	}()

	rnr, cancel := testRunner(t)
	defer cancel()
	ds := datastore.NewMockInit(
		[]*models.App{
			{Name: "myapp"},
			{Name: "myapp2"},
			{Name: "myapp3"},
		},
	)
	fnl := logs.NewMock()
	srv := testServer(ds, &mqs.Mock{}, fnl, rnr, ServerTypeFull)

	a1b := base64.RawURLEncoding.EncodeToString([]byte("myapp"))
	a2b := base64.RawURLEncoding.EncodeToString([]byte("myapp2"))
	a3b := base64.RawURLEncoding.EncodeToString([]byte("myapp3"))

	for i, test := range []struct {
		path          string
		body          string
		expectedCode  int
		expectedError error
		expectedLen   int
		nextCursor    string
	}{
		{"/v2/apps?per_page", "", http.StatusOK, nil, 3, ""},
		{"/v2/apps?per_page=1", "", http.StatusOK, nil, 1, a1b},
		{"/v2/apps?per_page=1&cursor=" + a1b, "", http.StatusOK, nil, 1, a2b},
		{"/v2/apps?per_page=1&cursor=" + a2b, "", http.StatusOK, nil, 1, a3b},
		{"/v2/apps?per_page=100&cursor=" + a2b, "", http.StatusOK, nil, 1, ""}, // cursor is empty if per_page > len(results)
		{"/v2/apps?per_page=1&cursor=" + a3b, "", http.StatusOK, nil, 0, ""},   // cursor could point to empty page
	} {
		_, rec := routerRequest(t, srv.Router, "GET", test.path, nil)

		if rec.Code != test.expectedCode {
			t.Errorf("Test %d: Expected status code to be %d but was %d",
				i, test.expectedCode, rec.Code)
		}

		if test.expectedError != nil {
			resp := getErrorResponse(t, rec)

			if !strings.Contains(resp.Message, test.expectedError.Error()) {
				t.Errorf("Test %d: Expected error message to have `%s`",
					i, test.expectedError.Error())
			}
		} else {
			// normal path

			var resp models.AppList
			err := json.NewDecoder(rec.Body).Decode(&resp)
			if err != nil {
				t.Errorf("Test %d: Expected response body to be a valid json object. err: %v", i, err)
			}
			if len(resp.Items) != test.expectedLen {
				t.Errorf("Test %d: Expected apps length to be %d, but got %d", i, test.expectedLen, len(resp.Items))
			}
			if resp.NextCursor != test.nextCursor {
				t.Errorf("Test %d: Expected next_cursor to be %s, but got %s", i, test.nextCursor, resp.NextCursor)
			}
		}
	}
}

func TestAppGet(t *testing.T) {
	buf := setLogBuffer()
	defer func() {
		if t.Failed() {
			t.Log(buf.String())
		}
	}()

	rnr, cancel := testRunner(t)
	defer cancel()
	app := &models.App{
		ID:   "appId",
		Name: "app",
	}
	ds := datastore.NewMockInit([]*models.App{app})
	fnl := logs.NewMock()
	srv := testServer(ds, &mqs.Mock{}, fnl, rnr, ServerTypeFull)

	for i, test := range []struct {
		path          string
		body          string
		expectedCode  int
		expectedError error
	}{
		{"/v2/apps/unknownApp", "", http.StatusNotFound, models.ErrAppsNotFound},
		{"/v2/apps/appId", "", http.StatusOK, nil},
	} {
		_, rec := routerRequest(t, srv.Router, "GET", test.path, nil)

		if rec.Code != test.expectedCode {
			t.Errorf("Test %d: Expected status code to be %d but was %d",
				i, test.expectedCode, rec.Code)
		}

		if test.expectedError != nil {
			resp := getErrorResponse(t, rec)

			if !strings.Contains(resp.Message, test.expectedError.Error()) {
				t.Errorf("Test %d: Expected error message to have `%s`",
					i, test.expectedError.Error())
			}
		}
	}
}

func TestAppUpdate(t *testing.T) {
	buf := setLogBuffer()
	defer func() {
		if t.Failed() {
			t.Log(buf.String())
		}
	}()

	app := &models.App{
		Name: "myapp",
		ID:   "appId",
	}
	ds := datastore.NewMockInit([]*models.App{app})

	for i, test := range []struct {
		mock          models.Datastore
		logDB         models.LogStore
		path          string
		body          string
		expectedCode  int
		expectedError error
	}{
		// errors
		{ds, logs.NewMock(), "/v2/apps/not_app", `{ }`, http.StatusNotFound, models.ErrAppsNotFound},

		{ds, logs.NewMock(), "/v2/apps/appId", ``, http.StatusBadRequest, models.ErrInvalidJSON},

		// Addresses #380
		{ds, logs.NewMock(), "/v2/apps/appId", `{  "name": "othername" }`, http.StatusConflict, models.ErrAppsNameImmutable},

		// success: add/set MD key
		{ds, logs.NewMock(), "/v2/apps/appId", `{ "annotations":{"foo":"bar"}}`, http.StatusOK, nil},

		// success
		{ds, logs.NewMock(), "/v2/apps/appId", `{  "config": { "test": "1" }  }`, http.StatusOK, nil},

		// success
		{ds, logs.NewMock(), "/v2/apps/appId", `{  "config": { "test": "1" } }`, http.StatusOK, nil},

		// success
		{ds, logs.NewMock(), "/v2/apps/appId", `{ "syslog_url":"tcp://example.com:443" }`, http.StatusOK, nil},
	} {
		t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
			rnr, cancel := testRunner(t)
			defer cancel()
			srv := testServer(test.mock, &mqs.Mock{}, test.logDB, rnr, ServerTypeFull)

			body := bytes.NewBuffer([]byte(test.body))
			_, rec := routerRequest(t, srv.Router, "PUT", test.path, body)

			if rec.Code != test.expectedCode {
				t.Fatalf("Test %d: Expected status code to be %d but was %d",
					i, test.expectedCode, rec.Code)
			}

			if test.expectedError != nil {
				fmt.Printf("resp: %s", rec.Body)
				resp := getErrorResponse(t, rec)

				if !strings.Contains(resp.Message, test.expectedError.Error()) {
					t.Errorf("Test %d: Expected error message to have `%s` but was `%s`",
						i, test.expectedError.Error(), resp.Message)
				}
			}

			if test.expectedCode == http.StatusOK {
				var app models.App
				err := json.NewDecoder(rec.Body).Decode(&app)
				if err != nil {
					t.Log(buf.String())
					t.Errorf("Test %d: error decoding body for 'ok' json, it was a lie: %v", i, err)
				}

				// IsZero() doesn't really work, this ensures it's not unset as long as we're not in 1970
				if time.Time(app.UpdatedAt).Before(time.Now().Add(-1 * time.Hour)) {
					t.Log(buf.String())
					t.Errorf("Test %d: expected updated_at to be set on app, it wasn't: %s", i, app.UpdatedAt)
				}

				// this isn't perfect, since a PATCH could succeed without updating any
				// fields (among other reasons), but just don't make a test for that or
				// special case (the body or smth) to ignore it here!
				// this is a decent approximation that the timestamp gets changed
				if (time.Time(app.UpdatedAt)).Equal(time.Time(app.CreatedAt)) {
					t.Log(buf.String())
					t.Errorf("Test %d: expected updated_at to not be the same as created at, it wasn't: %s %s", i, app.CreatedAt, app.UpdatedAt)
				}
			}

		})

	}
}
