package api

import (
	"bytes"
	"errors"
	"mime/multipart"
	"net/http"
	"net/http/httptest"
	"os"
	"reflect"
	"strings"
	"testing"
	"time"

	"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
	"github.com/labstack/echo/v4"
	"go.uber.org/zap"
)

func TestNewContext(t *testing.T) {
	for i, tc := range []struct {
		request          *http.Request
		expectErr        bool
		expectHTTPErr    bool
		expectHTTPStatus int
	}{
		{
			request:          httptest.NewRequest(http.MethodPost, "/", nil),
			expectErr:        true,
			expectHTTPErr:    true,
			expectHTTPStatus: http.StatusUnsupportedMediaType,
		},
		{
			request: func() *http.Request {
				req := httptest.NewRequest(http.MethodPost, "/", nil)
				req.Header.Set(echo.HeaderContentType, echo.MIMEMultipartForm)

				return req
			}(),
			expectErr:        true,
			expectHTTPErr:    true,
			expectHTTPStatus: http.StatusUnsupportedMediaType,
		},
		{
			request: func() *http.Request {
				body := &bytes.Buffer{}

				writer := multipart.NewWriter(body)

				defer func() {
					err := writer.Close()
					if err != nil {
						t.Fatalf("expected no error but got: %v", err)
					}
				}()

				err := writer.WriteField("foo", "foo")
				if err != nil {
					t.Fatalf("expected no error but got: %v", err)
				}

				req := httptest.NewRequest(http.MethodPost, "/", nil)
				req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())

				return req
			}(),
			expectErr:        true,
			expectHTTPErr:    true,
			expectHTTPStatus: http.StatusBadRequest,
		},
		{
			request: func() *http.Request {
				body := &bytes.Buffer{}

				writer := multipart.NewWriter(body)

				defer func() {
					err := writer.Close()
					if err != nil {
						t.Fatalf("expected no error but got: %v", err)
					}
				}()

				err := writer.WriteField("foo", "foo")
				if err != nil {
					t.Fatalf("expected no error but got: %v", err)
				}

				part, err := writer.CreateFormFile("foo.txt", "foo.txt")
				if err != nil {
					t.Fatalf("expected no error but got: %v", err)
				}

				_, err = part.Write([]byte("foo"))
				if err != nil {
					t.Fatalf("expected no error but got: %v", err)
				}

				req := httptest.NewRequest(http.MethodPost, "/", body)
				req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())

				return req
			}(),
		},
	} {
		handler := func(c echo.Context) error {
			_, cancel, err := newContext(c, zap.NewNop(), time.Duration(10)*time.Second)
			defer cancel()
			// Context already cancelled.
			defer cancel()

			if err != nil {
				return err
			}

			return nil
		}

		recorder := httptest.NewRecorder()

		srv := echo.New()
		srv.HideBanner = true
		srv.HidePort = true

		c := srv.NewContext(tc.request, recorder)
		err := handler(c)

		if tc.expectErr && err == nil {
			t.Errorf("test %d: expected error but got: %v", i, err)
		}

		if !tc.expectErr && err != nil {
			t.Errorf("test %d: expected no error but got: %v", i, err)
		}

		var httpErr HTTPError
		isHTTPErr := errors.As(err, &httpErr)

		if tc.expectHTTPErr && !isHTTPErr {
			t.Errorf("test %d: expected HTTP error but got: %v", i, err)
		}

		if !tc.expectHTTPErr && isHTTPErr {
			t.Errorf("test %d: expected no HTTP error but got one: %v", i, httpErr)
		}

		if err != nil && tc.expectHTTPErr && isHTTPErr {
			status, _ := httpErr.HTTPError()
			if status != tc.expectHTTPStatus {
				t.Errorf("test %d: expected %d HTTP status code but got %d", i, tc.expectHTTPStatus, status)
			}
		}
	}
}

func TestContext_Request(t *testing.T) {
	request := httptest.NewRequest(http.MethodPost, "/", nil)
	recorder := httptest.NewRecorder()
	c := echo.New().NewContext(request, recorder)

	ctx := Context{
		echoCtx: c,
	}

	if !reflect.DeepEqual(ctx.Request(), c.Request()) {
		t.Errorf("expected %v but got %v", ctx.Request(), c.Request())
	}
}

func TestContext_FormData(t *testing.T) {
	ctx := Context{
		values: map[string][]string{
			"foo": {"foo"},
		},
		files: map[string]string{
			"foo.txt": "/foo.txt",
		},
	}

	actual := ctx.FormData()
	expect := &FormData{
		values: ctx.values,
		files:  ctx.files,
	}

	if !reflect.DeepEqual(actual, expect) {
		t.Errorf("expected %+v but got %+v", expect, actual)
	}
}

func TestContext_GeneratePath(t *testing.T) {
	ctx := Context{
		dirPath: "/foo",
	}

	path := ctx.GeneratePath(".pdf")

	if !strings.HasPrefix(path, ctx.dirPath) {
		t.Errorf("expected '%s' to start with '%s'", path, ctx.dirPath)
	}
}

func TestContext_AddOutputPaths(t *testing.T) {
	for i, tc := range []struct {
		ctx         *Context
		path        string
		expectCount int
		expectErr   bool
	}{
		{
			ctx:       &Context{cancelled: true},
			expectErr: true,
		},
		{
			ctx:       &Context{dirPath: "/foo"},
			path:      "/bar/foo.txt",
			expectErr: true,
		},
		{
			ctx:         &Context{dirPath: "/foo"},
			path:        "/foo/foo.txt",
			expectCount: 1,
		},
	} {
		err := tc.ctx.AddOutputPaths(tc.path)

		if tc.expectErr && err == nil {
			t.Errorf("test %d: expected error but got: %v", i, err)
		}

		if !tc.expectErr && err != nil {
			t.Errorf("test %d: expected no error but got: %v", i, err)
		}

		if len(tc.ctx.outputPaths) != tc.expectCount {
			t.Errorf("test %d: expected %d output paths but got %d", i, tc.expectCount, len(tc.ctx.outputPaths))
		}
	}
}

func TestContext_Log(t *testing.T) {
	expect := zap.NewNop()
	ctx := Context{logger: expect}
	actual := ctx.Log()

	if !reflect.DeepEqual(actual, expect) {
		t.Errorf("expected %v but got %v", expect, actual)
	}
}

func TestContext_BuildOutputFile(t *testing.T) {
	for i, tc := range []struct {
		ctx       *Context
		expectErr bool
	}{
		{
			ctx:       &Context{cancelled: true},
			expectErr: true,
		},
		{
			ctx:       &Context{},
			expectErr: true,
		},
		{
			ctx: &Context{outputPaths: []string{"foo.txt"}},
		},
		{
			ctx:       &Context{outputPaths: []string{"foo.txt", "foo.pdf"}},
			expectErr: true,
		},
		{
			ctx: &Context{
				outputPaths: []string{
					"/tests/test/testdata/api/sample1.txt",
					"/tests/test/testdata/api/sample1.txt",
				},
			},
		},
	} {
		dirPath, err := gotenberg.MkdirAll()
		if err != nil {
			t.Fatalf("%d: expected no erro but got: %v", i, err)
		}

		tc.ctx.dirPath = dirPath
		tc.ctx.logger = zap.NewNop()

		_, err = tc.ctx.BuildOutputFile()

		if tc.expectErr && err == nil {
			t.Errorf("test %d: expected error but got: %v", i, err)
		}

		if !tc.expectErr && err != nil {
			t.Errorf("test %d: expected no error but got: %v", i, err)
		}

		err = os.RemoveAll(dirPath)
		if err != nil {
			t.Fatalf("%d: expected no erro but got: %v", i, err)
		}
	}
}

func TestContext_OutputFilename(t *testing.T) {
	for i, tc := range []struct {
		ctx                  *Context
		outputPath           string
		expectOutputFilename string
	}{
		{
			ctx: func() *Context {
				c := echo.New().NewContext(httptest.NewRequest(http.MethodGet, "/foo", nil), nil)
				c.Request().Header.Set("Gotenberg-Output-Filename", "foo")

				return &Context{echoCtx: c}
			}(),
			outputPath:           "/foo/bar.txt",
			expectOutputFilename: "foo.txt",
		},
		{
			ctx: func() *Context {
				c := echo.New().NewContext(httptest.NewRequest(http.MethodGet, "/foo", nil), nil)
				return &Context{echoCtx: c}
			}(),
			outputPath:           "/foo/foo.txt",
			expectOutputFilename: "foo.txt",
		},
	} {
		actual := tc.ctx.OutputFilename(tc.outputPath)

		if actual != tc.expectOutputFilename {
			t.Errorf("test %d: expected '%s' but got '%s'", i, tc.expectOutputFilename, actual)
		}
	}
}

func TestMockContext_SetDirPath(t *testing.T) {
	mock := &MockContext{&Context{}}
	mock.SetDirPath("/foo")

	actual := mock.dirPath
	expect := "/foo"

	if actual != expect {
		t.Errorf("expected '%s' but got '%s'", expect, actual)
	}
}

func TestMockContext_SetValues(t *testing.T) {
	mock := &MockContext{&Context{}}
	mock.SetValues(map[string][]string{
		"foo": {"foo"},
	})

	actual := mock.values
	expect := map[string][]string{
		"foo": {"foo"},
	}

	if !reflect.DeepEqual(actual, expect) {
		t.Errorf("expected %+v but got: %+v", expect, actual)
	}
}

func TestMockContext_SetFiles(t *testing.T) {
	mock := &MockContext{&Context{}}
	mock.SetFiles(map[string]string{
		"foo": "/foo",
	})

	actual := mock.files
	expect := map[string]string{
		"foo": "/foo",
	}

	if !reflect.DeepEqual(actual, expect) {
		t.Errorf("expected %+v but got: %+v", expect, actual)
	}
}

func TestMockContext_SetCancelled(t *testing.T) {
	mock := &MockContext{&Context{}}
	mock.SetCancelled(true)

	actual := mock.cancelled

	if !actual {
		t.Errorf("expected %t but got %t", true, actual)
	}
}

func TestMockContext_OutputPaths(t *testing.T) {
	mock := MockContext{
		&Context{
			outputPaths: []string{"/foo"},
		},
	}

	actual := mock.OutputPaths()
	expect := []string{"/foo"}

	if !reflect.DeepEqual(actual, expect) {
		t.Errorf("expected %+v but got: %+v", expect, actual)
	}
}

func TestMockContext_SetLogger(t *testing.T) {
	mock := MockContext{&Context{}}

	expect := zap.NewNop()
	mock.SetLogger(expect)

	actual := mock.logger

	if actual != expect {
		t.Errorf("expected %v but got %v", expect, actual)
	}
}

func TestMockContext_SetEchoContext(t *testing.T) {
	mock := MockContext{&Context{}}

	expect := echo.New().NewContext(nil, nil)
	mock.SetEchoContext(expect)

	actual := mock.echoCtx

	if actual != expect {
		t.Errorf("expected %v but got %v", expect, actual)
	}
}
