package provider_test

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strconv"
	"strings"
	"sync"
	"testing"

	"github.com/stretchr/testify/require"
)

type giteaAPITestServer struct {
	server         *httptest.Server
	requiredToken  string
	mu             sync.Mutex
	nextHookID     int64
	nextCreateCode int
	nextCreateBody string
	organizations  map[string]string
	repositories   map[string]giteaAPITestRepository
	hooksByTarget  map[string][]giteaAPITestHook
	orgLookups     map[string]int
	repoLookups    map[string]int
	lastWebhookURL string
	lastSecret     string
}

type giteaAPITestRepository struct {
	Owner    string
	Name     string
	FullName string
}

type giteaAPITestHook struct {
	ID     int64 `json:"id"`
	Config struct {
		URL string `json:"url"`
	} `json:"config"`
}

func newGiteaAPITestServer(t *testing.T, requiredToken string) *giteaAPITestServer {
	t.Helper()

	state := &giteaAPITestServer{
		requiredToken: strings.TrimSpace(requiredToken),
		nextHookID:    100,
		organizations: map[string]string{
			"acme": "acme",
		},
		repositories: map[string]giteaAPITestRepository{
			"acme/agentteam": {Owner: "acme", Name: "agentteam", FullName: "acme/agentteam"},
		},
		hooksByTarget: make(map[string][]giteaAPITestHook),
		orgLookups:    make(map[string]int),
		repoLookups:   make(map[string]int),
	}
	state.server = httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		if got := strings.TrimSpace(request.Header.Get("Authorization")); got != "token "+state.requiredToken {
			http.Error(writer, "unauthorized", http.StatusUnauthorized)
			return
		}

		if organization, remainder, ok := parseGiteaAPITestOrganizationPath(request.URL.Path); ok {
			state.mu.Lock()
			canonicalOrganization, found := state.canonicalOrganizationLocked(organization)
			state.mu.Unlock()
			if !found || canonicalOrganization != organization {
				http.NotFound(writer, request)
				return
			}
			switch {
			case request.Method == http.MethodGet && remainder == "":
				state.mu.Lock()
				state.orgLookups[canonicalOrganization]++
				state.mu.Unlock()
				writer.Header().Set("Content-Type", "application/json")
				require.NoError(t, json.NewEncoder(writer).Encode(map[string]string{
					"username": canonicalOrganization,
				}))
			default:
				state.handleHookRequest(t, writer, request, canonicalOrganization, remainder)
			}
			return
		}

		if repository, remainder, ok := parseGiteaAPITestRepositoryPath(request.URL.Path); ok {
			state.mu.Lock()
			canonicalRepository, found := state.canonicalRepositoryLocked(repository)
			state.mu.Unlock()
			if !found || canonicalRepository.FullName != repository {
				http.NotFound(writer, request)
				return
			}
			switch {
			case request.Method == http.MethodGet && remainder == "":
				state.mu.Lock()
				state.repoLookups[canonicalRepository.FullName]++
				state.mu.Unlock()
				writer.Header().Set("Content-Type", "application/json")
				require.NoError(t, json.NewEncoder(writer).Encode(map[string]any{
					"full_name": canonicalRepository.FullName,
					"name":      canonicalRepository.Name,
					"owner": map[string]string{
						"login": canonicalRepository.Owner,
					},
				}))
			default:
				state.handleHookRequest(t, writer, request, canonicalRepository.FullName, remainder)
			}
			return
		}

		http.NotFound(writer, request)
	}))
	return state
}

func parseGiteaAPITestOrganizationPath(path string) (string, string, bool) {
	const prefix = "/api/v1/orgs/"
	if !strings.HasPrefix(path, prefix) {
		return "", "", false
	}
	remainder := strings.TrimPrefix(path, prefix)
	if remainder == "" {
		return "", "", false
	}
	parts := strings.SplitN(remainder, "/", 2)
	if len(parts) == 1 {
		return parts[0], "", true
	}
	return parts[0], "/" + parts[1], true
}

func parseGiteaAPITestRepositoryPath(path string) (string, string, bool) {
	const prefix = "/api/v1/repos/"
	if !strings.HasPrefix(path, prefix) {
		return "", "", false
	}
	remainder := strings.TrimPrefix(path, prefix)
	if remainder == "" {
		return "", "", false
	}
	parts := strings.Split(remainder, "/")
	if len(parts) < 2 {
		return "", "", false
	}
	repository := parts[0] + "/" + parts[1]
	if len(parts) == 2 {
		return repository, "", true
	}
	return repository, "/" + strings.Join(parts[2:], "/"), true
}

func (server *giteaAPITestServer) handleHookRequest(t *testing.T, writer http.ResponseWriter, request *http.Request, target string, remainder string) {
	t.Helper()

	switch {
	case request.Method == http.MethodGet && remainder == "/hooks":
		server.mu.Lock()
		hooks := append([]giteaAPITestHook(nil), server.hooksByTarget[target]...)
		server.mu.Unlock()
		writer.Header().Set("Content-Type", "application/json")
		require.NoError(t, json.NewEncoder(writer).Encode(hooks))
	case request.Method == http.MethodPost && remainder == "/hooks":
		var payload struct {
			Events []string `json:"events"`
			Config struct {
				URL    string `json:"url"`
				Secret string `json:"secret"`
			} `json:"config"`
		}
		require.NoError(t, json.NewDecoder(request.Body).Decode(&payload))
		require.NotEmpty(t, strings.TrimSpace(payload.Config.URL))
		require.NotEmpty(t, strings.TrimSpace(payload.Config.Secret))
		require.ElementsMatch(t, []string{
			"pull_request",
			"pull_request_review",
			"pull_request_comment",
			"issue_comment",
			"push",
			"status",
			"workflow_run",
			"workflow_job",
		}, payload.Events)

		server.mu.Lock()
		if server.nextCreateCode != 0 {
			statusCode := server.nextCreateCode
			responseBody := strings.TrimSpace(server.nextCreateBody)
			server.nextCreateCode = 0
			server.nextCreateBody = ""
			server.mu.Unlock()
			if responseBody == "" {
				http.Error(writer, "create failed", statusCode)
				return
			}
			writer.Header().Set("Content-Type", "application/json")
			writer.WriteHeader(statusCode)
			_, err := writer.Write([]byte(responseBody))
			require.NoError(t, err)
			return
		}
		server.nextHookID++
		hook := giteaAPITestHook{ID: server.nextHookID}
		hook.Config.URL = strings.TrimSpace(payload.Config.URL)
		server.hooksByTarget[target] = append(server.hooksByTarget[target], hook)
		server.lastWebhookURL = hook.Config.URL
		server.lastSecret = strings.TrimSpace(payload.Config.Secret)
		server.mu.Unlock()

		writer.Header().Set("Content-Type", "application/json")
		require.NoError(t, json.NewEncoder(writer).Encode(hook))
	case request.Method == http.MethodDelete && strings.HasPrefix(remainder, "/hooks/"):
		idText := strings.TrimPrefix(remainder, "/hooks/")
		hookID, err := strconv.ParseInt(idText, 10, 64)
		require.NoError(t, err)

		server.mu.Lock()
		hooks := server.hooksByTarget[target]
		filtered := hooks[:0]
		for _, hook := range hooks {
			if hook.ID != hookID {
				filtered = append(filtered, hook)
			}
		}
		server.hooksByTarget[target] = append([]giteaAPITestHook(nil), filtered...)
		server.mu.Unlock()
		writer.WriteHeader(http.StatusNoContent)
	default:
		http.NotFound(writer, request)
	}
}

func (server *giteaAPITestServer) Close() {
	server.server.Close()
}

func (server *giteaAPITestServer) URL() string {
	return server.server.URL
}

func (server *giteaAPITestServer) hookCount(target string) int {
	server.mu.Lock()
	defer server.mu.Unlock()
	resolvedTarget, ok := server.canonicalTargetLocked(target)
	if !ok {
		return 0
	}
	return len(server.hooksByTarget[resolvedTarget])
}

func (server *giteaAPITestServer) addHook(target string, hookID int64, callbackURL string) {
	server.mu.Lock()
	defer server.mu.Unlock()
	resolvedTarget, ok := server.canonicalTargetLocked(target)
	if !ok || hookID <= 0 {
		return
	}
	hook := giteaAPITestHook{ID: hookID}
	hook.Config.URL = strings.TrimSpace(callbackURL)
	server.hooksByTarget[resolvedTarget] = append(server.hooksByTarget[resolvedTarget], hook)
}

func (server *giteaAPITestServer) hookIDs(target string) []int64 {
	server.mu.Lock()
	defer server.mu.Unlock()
	resolvedTarget, ok := server.canonicalTargetLocked(target)
	if !ok {
		return nil
	}
	ids := make([]int64, 0, len(server.hooksByTarget[resolvedTarget]))
	for _, hook := range server.hooksByTarget[resolvedTarget] {
		ids = append(ids, hook.ID)
	}
	return ids
}

func (server *giteaAPITestServer) orgLookupCount(org string) int {
	server.mu.Lock()
	defer server.mu.Unlock()
	canonicalOrganization, ok := server.canonicalOrganizationLocked(org)
	if !ok {
		return 0
	}
	return server.orgLookups[canonicalOrganization]
}

func (server *giteaAPITestServer) repoLookupCount(repository string) int {
	server.mu.Lock()
	defer server.mu.Unlock()
	canonicalRepository, ok := server.canonicalRepositoryLocked(repository)
	if !ok {
		return 0
	}
	return server.repoLookups[canonicalRepository.FullName]
}

func (server *giteaAPITestServer) setNextHookCreateStatus(statusCode int) {
	server.mu.Lock()
	defer server.mu.Unlock()
	server.nextCreateCode = statusCode
	server.nextCreateBody = ""
}

func (server *giteaAPITestServer) setNextHookCreateResponse(statusCode int, body string) {
	server.mu.Lock()
	defer server.mu.Unlock()
	server.nextCreateCode = statusCode
	server.nextCreateBody = body
}

func (server *giteaAPITestServer) allowOrganization(org string) {
	server.mu.Lock()
	defer server.mu.Unlock()
	trimmedOrganization := strings.TrimSpace(org)
	if trimmedOrganization == "" {
		return
	}
	server.organizations[strings.ToLower(trimmedOrganization)] = trimmedOrganization
}

func (server *giteaAPITestServer) allowRepository(repository string) {
	server.mu.Lock()
	defer server.mu.Unlock()
	repo, ok := canonicalizeGiteaAPITestRepository(repository)
	if !ok {
		return
	}
	server.repositories[strings.ToLower(repo.FullName)] = repo
}

func (server *giteaAPITestServer) canonicalOrganizationLocked(org string) (string, bool) {
	canonicalOrganization, ok := server.organizations[strings.ToLower(strings.TrimSpace(org))]
	return canonicalOrganization, ok
}

func (server *giteaAPITestServer) canonicalRepositoryLocked(repository string) (giteaAPITestRepository, bool) {
	repo, ok := server.repositories[strings.ToLower(strings.TrimSpace(repository))]
	return repo, ok
}

func (server *giteaAPITestServer) canonicalTargetLocked(target string) (string, bool) {
	if strings.Contains(strings.TrimSpace(target), "/") {
		repository, ok := server.canonicalRepositoryLocked(target)
		if !ok {
			return "", false
		}
		return repository.FullName, true
	}
	organization, ok := server.canonicalOrganizationLocked(target)
	if !ok {
		return "", false
	}
	return organization, true
}

func canonicalizeGiteaAPITestRepository(value string) (giteaAPITestRepository, bool) {
	parts := strings.Split(strings.TrimSpace(value), "/")
	if len(parts) != 2 {
		return giteaAPITestRepository{}, false
	}
	owner := strings.TrimSpace(parts[0])
	name := strings.TrimSpace(parts[1])
	if owner == "" || name == "" {
		return giteaAPITestRepository{}, false
	}
	return giteaAPITestRepository{
		Owner:    owner,
		Name:     name,
		FullName: owner + "/" + name,
	}, true
}
