package test

import (
	"io"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"

	"github.com/gorilla/schema"
	"github.com/labstack/echo/v4"
	"github.com/stretchr/testify/require"
	"github.com/thomiceli/opengist/internal/config"
	"github.com/thomiceli/opengist/internal/db"
	"github.com/thomiceli/opengist/internal/git"
	"github.com/thomiceli/opengist/internal/index"
	"github.com/thomiceli/opengist/internal/web/context"
	"github.com/thomiceli/opengist/internal/web/handlers/metrics"
	"github.com/thomiceli/opengist/internal/web/server"
)

var databaseType string
var formEncoder *schema.Encoder

func init() {
	formEncoder = schema.NewEncoder()
	formEncoder.SetAliasTag("form")
}

type Server struct {
	server        *server.Server
	SessionCookie string
	contextData   echo.Map
}

func (s *Server) Request(t *testing.T, method, uri string, data interface{}, expectedCode int) *http.Response {
	return s.RequestWithHeaders(t, method, uri, data, expectedCode, nil)
}

func (s *Server) RequestWithHeaders(t *testing.T, method, uri string, data interface{}, expectedCode int, headers map[string]string) *http.Response {
	var bodyReader io.Reader
	if method == http.MethodPost || method == http.MethodPut || method == http.MethodDelete {
		if values, ok := data.(url.Values); ok {
			bodyReader = strings.NewReader(values.Encode())
		} else if data != nil {
			values := url.Values{}
			_ = formEncoder.Encode(data, values)
			bodyReader = strings.NewReader(values.Encode())
		}
	}

	req := httptest.NewRequest(method, uri, bodyReader)
	w := httptest.NewRecorder()

	if method == http.MethodPost || method == http.MethodPut {
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	}

	req.Header.Set("Sec-Fetch-Site", "same-origin")

	for key, value := range headers {
		req.Header.Set(key, value)
	}

	if s.SessionCookie != "" {
		req.AddCookie(&http.Cookie{Name: "session", Value: s.SessionCookie})
	}

	s.server.ServeHTTP(w, req)
	if expectedCode != 0 {
		require.Equalf(t, expectedCode, w.Code, "Unexpected status code for %s %s: got %d, expected %d", method, uri, w.Code, expectedCode)
	}
	if method == http.MethodPost {
		if strings.Contains(uri, "/login") {
			cookie := ""
			h := w.Header().Get("Set-Cookie")
			parts := strings.Split(h, "; ")
			for _, p := range parts {
				if strings.HasPrefix(p, "session=") {
					cookie = p
					break
				}
			}
			s.SessionCookie = strings.TrimPrefix(cookie, "session=")
		} else if strings.Contains(uri, "/logout") {
			s.SessionCookie = ""
		}
	}

	return w.Result()
}

func (s *Server) RawRequest(t *testing.T, req *http.Request, expectedCode int) *http.Response {
	w := httptest.NewRecorder()

	req.Header.Set("Sec-Fetch-Site", "same-origin")

	if s.SessionCookie != "" {
		req.AddCookie(&http.Cookie{Name: "session", Value: s.SessionCookie})
	}

	s.server.ServeHTTP(w, req)

	require.Equal(t, expectedCode, w.Code, "unexpected status code for %s %s", req.Method, req.URL.Path)

	return w.Result()
}

func (s *Server) StartHttpServer(t *testing.T) string {
	hs := httptest.NewServer(s.server)
	t.Cleanup(hs.Close)
	return hs.URL
}

func (s *Server) User() *db.User {
	s.Request(nil, "GET", "/", nil, 0)
	if user, ok := s.contextData["userLogged"].(*db.User); ok {
		return user
	}
	return nil
}

func (s *Server) TestCtxData(t *testing.T, expected echo.Map) {
	for key, expectedValue := range expected {
		actualValue, exists := s.contextData[key]
		require.True(t, exists, "Key %q not found in context data", key)
		require.Equal(t, expectedValue, actualValue, "Context data mismatch for key %q", key)
	}
}

func (s *Server) Register(t *testing.T, user string) {
	s.Request(t, "POST", "/register", db.UserDTO{Username: user, Password: user}, 302)
}

func (s *Server) Login(t *testing.T, user string) {
	s.Request(t, "POST", "/login", db.UserDTO{Username: user, Password: user}, 302)
}

func (s *Server) Logout() {
	s.SessionCookie = ""
}

func (s *Server) CreateGist(t *testing.T, visibility string) (gistPath string, gist *db.Gist, username, identifier string) {
	s.Request(t, "POST", "/register", db.UserDTO{Username: "thomas", Password: "thomas"}, 0)
	s.Login(t, "thomas")

	resp := s.Request(t, "POST", "/", url.Values{
		"title":   {"Test"},
		"name":    {"file.txt", "otherfile.txt"},
		"content": {"hello world", "other content"},
		"topics":  {"hello opengist"},
		"private": {visibility},
	}, 302)

	// Extract gist identifier from redirect
	location := resp.Header.Get("Location")
	parts := strings.Split(strings.TrimPrefix(location, "/"), "/")
	require.Len(t, parts, 2, "Expected redirect format: /{username}/{identifier}")

	gistUsername := parts[0]
	gistIdentifier := parts[1]

	gist, err := db.GetGist(gistUsername, gistIdentifier)
	require.NoError(t, err)
	require.NotNil(t, gist)

	gistPath = filepath.Join(config.GetHomeDir(), git.ReposDirectory, "thomas", gist.Uuid)

	// Verify gist exists on filesystem
	_, err = os.Stat(gistPath)
	require.NoError(t, err, "Gist repository should exist at %s", gistPath)

	username = gist.User.Username
	identifier = gist.Identifier()

	s.Logout()
	return gistPath, gist, username, identifier
}

// CreateGistAs creates a gist as the specified user. visibility "0"/"1"/"2".
// The caller does not need to be logged in beforehand; this method logs in as user.
func (s *Server) CreateGistAs(t *testing.T, user, visibility string) (string, *db.Gist, string, string) {
	s.Logout()
	s.Login(t, user)
	resp := s.Request(t, "POST", "/", url.Values{
		"title":   {"Test"},
		"name":    {"file.txt"},
		"content": {"hello"},
		"private": {visibility},
	}, 302)
	loc := resp.Header.Get("Location")
	parts := strings.Split(strings.TrimPrefix(loc, "/"), "/")
	require.Len(t, parts, 2)
	gist, err := db.GetGist(parts[0], parts[1])
	require.NoError(t, err)
	require.NotNil(t, gist)
	return "", gist, gist.User.Username, gist.Identifier()
}

func Setup(t *testing.T) *Server {
	tmpDir := t.TempDir()
	t.Setenv("OPENGIST_SKIP_GIT_HOOKS", "1")

	err := config.InitConfig("", io.Discard)
	require.NoError(t, err, "Could not init config")

	config.C.LogLevel = "warn"
	config.C.LogOutput = "stdout"
	config.C.GitDefaultBranch = "master"
	config.C.OpengistHome = tmpDir

	config.SetupSecretKey()
	config.InitLog()

	context.ManifestEntries = map[string]context.Asset{
		"embed.css":   {File: "assets/embed.css"},
		"ts/embed.ts": {Css: []string{"assets/embed.css"}},
		"ts/light.ts": {Css: []string{"assets/light.css"}},
		"ts/dark.ts":  {Css: []string{"assets/dark.css"}},
		"ts/auto.ts":  {Css: []string{"assets/auto.css"}},
	}

	tmpGitConfig := filepath.Join(tmpDir, "gitconfig")
	t.Setenv("GIT_CONFIG_GLOBAL", tmpGitConfig)

	err = exec.Command("git", "config", "--global", "--type", "bool", "push.autoSetupRemote", "true").Run()
	require.NoError(t, err)
	err = exec.Command("git", "config", "--global", "user.email", "test@opengist.io").Run()
	require.NoError(t, err)
	err = exec.Command("git", "config", "--global", "user.name", "test").Run()
	require.NoError(t, err)
	homePath := config.GetHomeDir()

	var databaseDsn string
	databaseType = os.Getenv("OPENGIST_TEST_DB")
	switch databaseType {
	case "postgres":
		databaseDsn = "postgres://postgres:opengist@localhost:5432/opengist_test"
	case "mysql":
		databaseDsn = "mysql://root:opengist@localhost:3306/opengist_test"
	default:
		databaseDsn = config.C.DBUri
	}

	err = os.MkdirAll(filepath.Join(homePath, "sessions"), 0755)
	require.NoError(t, err, "Could not create sessions directory")

	err = os.MkdirAll(filepath.Join(homePath, "repos"), 0755)
	require.NoError(t, err, "Could not create repos directory")

	err = os.MkdirAll(filepath.Join(homePath, "tmp", "repos"), 0755)
	require.NoError(t, err, "Could not create tmp repos directory")

	err = os.MkdirAll(filepath.Join(homePath, "custom"), 0755)
	require.NoError(t, err, "Could not create custom directory")

	err = db.Setup(databaseDsn)
	require.NoError(t, err, "Could not initialize database")

	t.Cleanup(func() {
		db.Close()
	})

	if index.IndexEnabled() {
		go index.NewIndexer(index.IndexType())
	}

	s := &Server{
		server: server.NewServer(true),
	}

	s.server.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			err := next(c)
			if data, ok := c.Request().Context().Value(context.DataKeyStr).(echo.Map); ok {
				s.contextData = data
			}
			return err
		}
	})

	return s
}

func Teardown(t *testing.T) {
	switch databaseType {
	case "postgres", "mysql":
		err := db.TruncateDatabase()
		require.NoError(t, err, "Could not truncate database")
	}
}

func NewTestMetricsServer() *metrics.Server {
	return metrics.NewServer()
}
