package apis_test

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"testing"

	"github.com/pocketbase/pocketbase/apis"
	"github.com/pocketbase/pocketbase/core"
	"github.com/pocketbase/pocketbase/tests"
	"github.com/pocketbase/pocketbase/tools/router"
)

func TestWrapStdHandler(t *testing.T) {
	t.Parallel()

	app, _ := tests.NewTestApp()
	defer app.Cleanup()

	req := httptest.NewRequest(http.MethodGet, "/", nil)
	rec := httptest.NewRecorder()

	e := new(core.RequestEvent)
	e.App = app
	e.Request = req
	e.Response = rec

	err := apis.WrapStdHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("test"))
	}))(e)
	if err != nil {
		t.Fatal(err)
	}

	if body := rec.Body.String(); body != "test" {
		t.Fatalf("Expected body %q, got %q", "test", body)
	}
}

func TestWrapStdMiddleware(t *testing.T) {
	t.Parallel()

	app, _ := tests.NewTestApp()
	defer app.Cleanup()

	req := httptest.NewRequest(http.MethodGet, "/", nil)
	rec := httptest.NewRecorder()

	e := new(core.RequestEvent)
	e.App = app
	e.Request = req
	e.Response = rec

	err := apis.WrapStdMiddleware(func(h http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			w.Write([]byte("test"))
		})
	})(e)
	if err != nil {
		t.Fatal(err)
	}

	if body := rec.Body.String(); body != "test" {
		t.Fatalf("Expected body %q, got %q", "test", body)
	}
}

func TestStatic(t *testing.T) {
	t.Parallel()

	app, _ := tests.NewTestApp()
	defer app.Cleanup()

	dir := createTestDir(t)
	defer os.RemoveAll(dir)

	fsys := os.DirFS(filepath.Join(dir, "sub"))

	type staticScenario struct {
		path           string
		indexFallback  bool
		expectedStatus int
		expectBody     string
		expectError    bool
	}

	scenarios := []staticScenario{
		{
			path:           "",
			indexFallback:  false,
			expectedStatus: 200,
			expectBody:     "sub index.html",
			expectError:    false,
		},
		{
			path:           "missing/a/b/c",
			indexFallback:  false,
			expectedStatus: 404,
			expectBody:     "",
			expectError:    true,
		},
		{
			path:           "missing/a/b/c",
			indexFallback:  true,
			expectedStatus: 200,
			expectBody:     "sub index.html",
			expectError:    false,
		},
		{
			path:           "testroot", // parent directory file
			indexFallback:  false,
			expectedStatus: 404,
			expectBody:     "",
			expectError:    true,
		},
		{
			path:           "test",
			indexFallback:  false,
			expectedStatus: 200,
			expectBody:     "sub test",
			expectError:    false,
		},
		{
			path:           "sub2",
			indexFallback:  false,
			expectedStatus: 301,
			expectBody:     "",
			expectError:    false,
		},
		{
			path:           "sub2/",
			indexFallback:  false,
			expectedStatus: 200,
			expectBody:     "sub2 index.html",
			expectError:    false,
		},
		{
			path:           "sub2/test",
			indexFallback:  false,
			expectedStatus: 200,
			expectBody:     "sub2 test",
			expectError:    false,
		},
		{
			path:           "sub2/test/",
			indexFallback:  false,
			expectedStatus: 301,
			expectBody:     "",
			expectError:    false,
		},
	}

	// extra directory traversal checks
	dtp := []string{
		"/../",
		"\\../",
		"../",
		"../../",
		"..\\",
		"..\\..\\",
		"../..\\",
		"..\\..//",
		`%2e%2e%2f`,
		`%2e%2e%2f%2e%2e%2f`,
		`%2e%2e/`,
		`%2e%2e/%2e%2e/`,
		`..%2f`,
		`..%2f..%2f`,
		`%2e%2e%5c`,
		`%2e%2e%5c%2e%2e%5c`,
		`%2e%2e\`,
		`%2e%2e\%2e%2e\`,
		`..%5c`,
		`..%5c..%5c`,
		`%252e%252e%255c`,
		`%252e%252e%255c%252e%252e%255c`,
		`..%255c`,
		`..%255c..%255c`,
	}
	for _, p := range dtp {
		scenarios = append(scenarios,
			staticScenario{
				path:           p + "testroot",
				indexFallback:  false,
				expectedStatus: 404,
				expectBody:     "",
				expectError:    true,
			},
			staticScenario{
				path:           p + "testroot",
				indexFallback:  true,
				expectedStatus: 200,
				expectBody:     "sub index.html",
				expectError:    false,
			},
		)
	}

	for i, s := range scenarios {
		t.Run(fmt.Sprintf("%d_%s_%v", i, s.path, s.indexFallback), func(t *testing.T) {
			req := httptest.NewRequest(http.MethodGet, "/"+s.path, nil)
			req.SetPathValue(apis.StaticWildcardParam, s.path)

			rec := httptest.NewRecorder()

			e := new(core.RequestEvent)
			e.App = app
			e.Request = req
			e.Response = rec

			err := apis.Static(fsys, s.indexFallback)(e)

			hasErr := err != nil
			if hasErr != s.expectError {
				t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err)
			}

			body := rec.Body.String()
			if body != s.expectBody {
				t.Fatalf("Expected body %q, got %q", s.expectBody, body)
			}

			if hasErr {
				apiErr := router.ToApiError(err)
				if apiErr.Status != s.expectedStatus {
					t.Fatalf("Expected status code %d, got %d", s.expectedStatus, apiErr.Status)
				}
			}
		})
	}
}

func TestMustSubFS(t *testing.T) {
	t.Parallel()

	dir := createTestDir(t)
	defer os.RemoveAll(dir)

	// invalid path (no beginning and ending slashes)
	if !hasPanicked(func() {
		apis.MustSubFS(os.DirFS(dir), "/test/")
	}) {
		t.Fatalf("Expected to panic")
	}

	// valid path
	if hasPanicked(func() {
		apis.MustSubFS(os.DirFS(dir), "./////a/b/c") // checks if ToSlash was called
	}) {
		t.Fatalf("Didn't expect to panic")
	}

	// check sub content
	sub := apis.MustSubFS(os.DirFS(dir), "sub")

	_, err := sub.Open("test")
	if err != nil {
		t.Fatalf("Missing expected file sub/test")
	}
}

// -------------------------------------------------------------------

func hasPanicked(f func()) (didPanic bool) {
	defer func() {
		if r := recover(); r != nil {
			didPanic = true
		}
	}()
	f()
	return
}

// note: make sure to call os.RemoveAll(dir) after you are done
// working with the created test dir.
func createTestDir(t *testing.T) string {
	dir, err := os.MkdirTemp(os.TempDir(), "test_dir")
	if err != nil {
		t.Fatal(err)
	}

	if err := os.WriteFile(filepath.Join(dir, "index.html"), []byte("root index.html"), 0644); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(dir, "testroot"), []byte("root test"), 0644); err != nil {
		t.Fatal(err)
	}

	if err := os.MkdirAll(filepath.Join(dir, "sub"), os.ModePerm); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(dir, "sub/index.html"), []byte("sub index.html"), 0644); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(dir, "sub/test"), []byte("sub test"), 0644); err != nil {
		t.Fatal(err)
	}

	if err := os.MkdirAll(filepath.Join(dir, "sub", "sub2"), os.ModePerm); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(dir, "sub/sub2/index.html"), []byte("sub2 index.html"), 0644); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(dir, "sub/sub2/test"), []byte("sub2 test"), 0644); err != nil {
		t.Fatal(err)
	}

	return dir
}