package testserver

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
	"testing"
	"time"

	"github.com/pkg/errors"
	// sqlite driver.
	_ "modernc.org/sqlite"

	"github.com/usememos/memos/api/auth"
	"github.com/usememos/memos/server"
	"github.com/usememos/memos/server/profile"
	"github.com/usememos/memos/store"
	"github.com/usememos/memos/store/db"
	"github.com/usememos/memos/test"
)

type TestingServer struct {
	server  *server.Server
	client  *http.Client
	profile *profile.Profile
	cookie  string
}

func NewTestingServer(ctx context.Context, t *testing.T) (*TestingServer, error) {
	profile := test.GetTestingProfile(t)
	dbDriver, err := db.NewDBDriver(profile)
	if err != nil {
		return nil, errors.Wrap(err, "failed to create db driver")
	}
	if err := dbDriver.Migrate(ctx); err != nil {
		return nil, errors.Wrap(err, "failed to migrate db")
	}

	store := store.New(dbDriver, profile)
	server, err := server.NewServer(ctx, profile, store)
	if err != nil {
		return nil, errors.Wrap(err, "failed to create server")
	}

	s := &TestingServer{
		server:  server,
		client:  &http.Client{},
		profile: profile,
		cookie:  "",
	}
	errChan := make(chan error, 1)

	go func() {
		if err := s.server.Start(ctx); err != nil {
			if err != http.ErrServerClosed {
				errChan <- errors.Wrap(err, "failed to run main server")
			}
		}
	}()

	if err := s.waitForServerStart(errChan); err != nil {
		return nil, errors.Wrap(err, "failed to start server")
	}

	return s, nil
}

func (s *TestingServer) Shutdown(ctx context.Context) {
	s.server.Shutdown(ctx)
}

func (s *TestingServer) waitForServerStart(errChan <-chan error) error {
	ticker := time.NewTicker(100 * time.Millisecond)
	defer ticker.Stop()

	for {
		select {
		case <-ticker.C:
			if s == nil {
				continue
			}
			e := s.server.GetEcho()
			if e == nil {
				continue
			}
			addr := e.ListenerAddr()
			if addr != nil && strings.Contains(addr.String(), ":") {
				return nil // was started
			}
		case err := <-errChan:
			if err == http.ErrServerClosed {
				return nil
			}
			return err
		}
	}
}

func (s *TestingServer) request(method, uri string, body io.Reader, params, header map[string]string) (io.ReadCloser, error) {
	fullURL := fmt.Sprintf("http://localhost:%d%s", s.profile.Port, uri)
	req, err := http.NewRequest(method, fullURL, body)
	if err != nil {
		return nil, errors.Wrapf(err, "fail to create a new %s request(%q)", method, fullURL)
	}

	for k, v := range header {
		req.Header.Set(k, v)
	}

	q := url.Values{}
	for k, v := range params {
		q.Add(k, v)
	}
	if len(q) > 0 {
		req.URL.RawQuery = q.Encode()
	}

	resp, err := s.client.Do(req)
	if err != nil {
		return nil, errors.Wrapf(err, "fail to send a %s request(%q)", method, fullURL)
	}
	if resp.StatusCode != http.StatusOK {
		body, err := io.ReadAll(resp.Body)
		if err != nil {
			return nil, errors.Wrap(err, "failed to read http response body")
		}
		return nil, errors.Errorf("http response error code %v body %q", resp.StatusCode, string(body))
	}

	if method == "POST" {
		if strings.Contains(uri, "/api/v1/auth/login") || strings.Contains(uri, "/api/v1/auth/signup") {
			cookie := ""
			h := resp.Header.Get("Set-Cookie")
			parts := strings.Split(h, "; ")
			for _, p := range parts {
				if strings.HasPrefix(p, fmt.Sprintf("%s=", auth.AccessTokenCookieName)) {
					cookie = p
					break
				}
			}
			if cookie == "" {
				return nil, errors.New("unable to find access token in the login response headers")
			}
			s.cookie = cookie
		} else if strings.Contains(uri, "/api/v1/auth/signout") {
			s.cookie = ""
		}
	}
	return resp.Body, nil
}

// get sends a GET client request.
func (s *TestingServer) get(url string, params map[string]string) (io.ReadCloser, error) {
	return s.request("GET", url, nil, params, map[string]string{
		"Cookie": s.cookie,
	})
}

// post sends a POST client request.
func (s *TestingServer) post(url string, body io.Reader, params map[string]string) (io.ReadCloser, error) {
	return s.request("POST", url, body, params, map[string]string{
		"Cookie": s.cookie,
	})
}

// patch sends a PATCH client request.
func (s *TestingServer) patch(url string, body io.Reader, params map[string]string) (io.ReadCloser, error) {
	return s.request("PATCH", url, body, params, map[string]string{
		"Cookie": s.cookie,
	})
}

// delete sends a DELETE client request.
func (s *TestingServer) delete(url string, params map[string]string) (io.ReadCloser, error) {
	return s.request("DELETE", url, nil, params, map[string]string{
		"Cookie": s.cookie,
	})
}
