package tests

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"github.com/labstack/echo/v5"
	"github.com/pocketbase/pocketbase/apis"
	"github.com/pocketbase/pocketbase/core"
)

// ApiScenario defines a single api request test case/scenario.
type ApiScenario struct {
	Name           string
	Method         string
	Url            string
	Body           io.Reader
	RequestHeaders map[string]string

	// Delay adds a delay before checking the expectations usually
	// to ensure that all fired non-awaited go routines have finished
	Delay time.Duration

	// expectations
	// ---
	ExpectedStatus     int
	ExpectedContent    []string
	NotExpectedContent []string
	ExpectedEvents     map[string]int

	// test hooks
	// ---
	TestAppFactory func() (*TestApp, error)
	BeforeTestFunc func(t *testing.T, app *TestApp, e *echo.Echo)
	AfterTestFunc  func(t *testing.T, app *TestApp, e *echo.Echo)
}

// Test executes the test scenario.
func (scenario *ApiScenario) Test(t *testing.T) {
	var name = scenario.Name
	if name == "" {
		name = fmt.Sprintf("%s:%s", scenario.Method, scenario.Url)
	}

	t.Run(name, scenario.test)
}

func (scenario *ApiScenario) test(t *testing.T) {
	var testApp *TestApp
	var testAppErr error
	if scenario.TestAppFactory != nil {
		// @todo consider passing the testing instance to the factory and maybe remove the error from the declaration
		// (see https://github.com/pocketbase/pocketbase/discussions/3025)
		testApp, testAppErr = scenario.TestAppFactory()
	} else {
		testApp, testAppErr = NewTestApp()
	}
	if testAppErr != nil {
		t.Fatalf("Failed to initialize the test app instance: %v", testAppErr)
	}
	defer testApp.Cleanup()

	e, err := apis.InitApi(testApp)
	if err != nil {
		t.Fatal(err)
	}

	// manually trigger the serve event to ensure that custom app routes and middlewares are registered
	testApp.OnBeforeServe().Trigger(&core.ServeEvent{
		App:    testApp,
		Router: e,
	})

	if scenario.BeforeTestFunc != nil {
		scenario.BeforeTestFunc(t, testApp, e)
	}

	recorder := httptest.NewRecorder()
	req := httptest.NewRequest(scenario.Method, scenario.Url, scenario.Body)

	// add middleware to timeout long-running requests (eg. keep-alive routes)
	e.Pre(func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			ctx, cancelFunc := context.WithTimeout(c.Request().Context(), 100*time.Millisecond)
			defer cancelFunc()
			c.SetRequest(c.Request().Clone(ctx))
			return next(c)
		}
	})

	// set default header
	req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)

	// set scenario headers
	for k, v := range scenario.RequestHeaders {
		req.Header.Set(k, v)
	}

	// execute request
	e.ServeHTTP(recorder, req)

	res := recorder.Result()

	if res.StatusCode != scenario.ExpectedStatus {
		t.Errorf("Expected status code %d, got %d", scenario.ExpectedStatus, res.StatusCode)
	}

	if scenario.Delay > 0 {
		time.Sleep(scenario.Delay)
	}

	if len(scenario.ExpectedContent) == 0 && len(scenario.NotExpectedContent) == 0 {
		if len(recorder.Body.Bytes()) != 0 {
			t.Errorf("Expected empty body, got \n%v", recorder.Body.String())
		}
	} else {
		// normalize json response format
		buffer := new(bytes.Buffer)
		err := json.Compact(buffer, recorder.Body.Bytes())
		var normalizedBody string
		if err != nil {
			// not a json...
			normalizedBody = recorder.Body.String()
		} else {
			normalizedBody = buffer.String()
		}

		for _, item := range scenario.ExpectedContent {
			if !strings.Contains(normalizedBody, item) {
				t.Errorf("Cannot find %v in response body \n%v", item, normalizedBody)
				break
			}
		}

		for _, item := range scenario.NotExpectedContent {
			if strings.Contains(normalizedBody, item) {
				t.Errorf("Didn't expect %v in response body \n%v", item, normalizedBody)
				break
			}
		}
	}

	// to minimize the breaking changes we always expect the error
	// events to be called on API error
	if res.StatusCode >= 400 {
		if scenario.ExpectedEvents == nil {
			scenario.ExpectedEvents = map[string]int{}
		}
		if _, ok := scenario.ExpectedEvents["OnBeforeApiError"]; !ok {
			scenario.ExpectedEvents["OnBeforeApiError"] = 1
		}
		if _, ok := scenario.ExpectedEvents["OnAfterApiError"]; !ok {
			scenario.ExpectedEvents["OnAfterApiError"] = 1
		}
	}

	if len(testApp.EventCalls) > len(scenario.ExpectedEvents) {
		t.Errorf("Expected events %v, got %v", scenario.ExpectedEvents, testApp.EventCalls)
	}

	for event, expectedCalls := range scenario.ExpectedEvents {
		actualCalls := testApp.EventCalls[event]
		if actualCalls != expectedCalls {
			t.Errorf("Expected event %s to be called %d, got %d", event, expectedCalls, actualCalls)
		}
	}

	// @todo consider adding the response body to the AfterTestFunc args
	if scenario.AfterTestFunc != nil {
		scenario.AfterTestFunc(t, testApp, e)
	}
}
