package httpserver_test

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"mime/multipart"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"testing"
)

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

	router, cfg, store := newTestRouterWithStore(t)

	sessionCookie := loginAsAdmin(t, router)

	projectLocation := submitForm(t, router, http.MethodPost, "/admin/projects", url.Values{
		"name":        {"Desktop App"},
		"slug":        {""},
		"description": {"Primary desktop updater stream."},
	}, http.StatusSeeOther, sessionCookie)
	projectID := extractResourceID(t, projectLocation, "/admin/projects/")

	submitForm(t, router, http.MethodPost, "/admin/projects/"+projectID, url.Values{
		"name":        {"Desktop App"},
		"slug":        {"desktop-app"},
		"description": {"Primary desktop updater stream with release uploads."},
	}, http.StatusSeeOther, sessionCookie)

	tagLocation := submitForm(t, router, http.MethodPost, "/admin/tags", url.Values{
		"name":        {"Windows Stable"},
		"slug":        {""},
		"description": {"Windows production releases."},
	}, http.StatusSeeOther, sessionCookie)
	tagID := extractResourceID(t, tagLocation, "/admin/tags/")

	submitForm(t, router, http.MethodPost, "/admin/projects/"+projectID+"/tags", url.Values{
		"tag_id": {tagID},
	}, http.StatusSeeOther, sessionCookie)

	artifactBody := []byte("release-payload-1.0.0")
	uploadLocation := submitMultipartForm(t, router, "/admin/projects/"+projectID+"/releases", map[string]string{
		"version":       "1.0.0",
		"build":         "build-42",
		"release_notes": "Initial Windows stable rollout.",
	}, "artifact", "..\\Desktop App 1.0.0.zip", artifactBody, sessionCookie)

	if !strings.Contains(uploadLocation, "status=release-uploaded") {
		t.Fatalf("expected release upload redirect status, got %q", uploadLocation)
	}

	projectIDInt := mustParseInt64(t, projectID)
	project, err := store.Projects.GetByID(context.Background(), projectIDInt)
	if err != nil {
		t.Fatalf("load project: %v", err)
	}

	if project.Slug != "desktop-app" {
		t.Fatalf("expected auto-generated project slug to persist, got %q", project.Slug)
	}

	if !project.IsActive {
		t.Fatal("expected project to remain active until the archive action runs")
	}

	projectTags, err := store.Projects.ListTags(context.Background(), project.ID)
	if err != nil {
		t.Fatalf("load project tags: %v", err)
	}

	if len(projectTags) != 1 || projectTags[0].Slug != "windows-stable" {
		t.Fatalf("expected one attached tag, got %+v", projectTags)
	}

	releases, err := store.Releases.ListByProjectID(context.Background(), project.ID)
	if err != nil {
		t.Fatalf("load releases: %v", err)
	}

	if len(releases) != 1 {
		t.Fatalf("expected one release, got %d", len(releases))
	}

	release := releases[0].Release
	if release.Filename != "desktop-app-1.0.0.zip" {
		t.Fatalf("expected sanitized filename, got %q", release.Filename)
	}

	expectedChecksum := sha256.Sum256(artifactBody)
	if release.ChecksumSHA256 != hex.EncodeToString(expectedChecksum[:]) {
		t.Fatalf("expected checksum %s, got %s", hex.EncodeToString(expectedChecksum[:]), release.ChecksumSHA256)
	}

	if release.StoragePath != "desktop-app/1.0.0/build-42/desktop-app-1.0.0.zip" {
		t.Fatalf("unexpected storage path %q", release.StoragePath)
	}

	artifactPath := filepath.Join(cfg.ArtifactsDir, filepath.FromSlash(release.StoragePath))
	if strings.HasPrefix(artifactPath, cfg.StaticDir) {
		t.Fatalf("artifact path %q should not be inside static dir %q", artifactPath, cfg.StaticDir)
	}

	storedArtifact, err := os.ReadFile(artifactPath)
	if err != nil {
		t.Fatalf("read artifact from disk: %v", err)
	}

	if !bytes.Equal(storedArtifact, artifactBody) {
		t.Fatal("stored artifact body did not match uploaded payload")
	}

	recorder := performRequest(t, router, http.MethodGet, "/admin/projects/"+projectID, nil, sessionCookie)
	if recorder.Code != http.StatusOK {
		t.Fatalf("expected project detail page to load, got %d", recorder.Code)
	}

	projectBody := recorder.Body.String()
	if !strings.Contains(projectBody, "/api/v1/projects/desktop-app/releases/latest") {
		t.Fatal("expected project detail page to show latest release metadata endpoint")
	}

	if !strings.Contains(projectBody, "/api/v1/releases/1/download") {
		t.Fatal("expected project detail page to show release download endpoint")
	}

	submitForm(t, router, http.MethodPost, "/admin/projects/"+projectID+"/archive", url.Values{
		"state": {"archive"},
	}, http.StatusSeeOther, sessionCookie)

	project, err = store.Projects.GetByID(context.Background(), projectIDInt)
	if err != nil {
		t.Fatalf("reload project after archive: %v", err)
	}

	if project.IsActive {
		t.Fatal("expected archived project to be inactive")
	}

	recorder = performRequest(t, router, http.MethodGet, "/admin/projects", nil, sessionCookie)
	if recorder.Code != http.StatusOK {
		t.Fatalf("expected projects page to load, got %d", recorder.Code)
	}

	if !strings.Contains(recorder.Body.String(), "Desktop App") {
		t.Fatal("expected projects page to list the created project")
	}

	recorder = performRequest(t, router, http.MethodGet, "/admin/tags", nil, sessionCookie)
	if recorder.Code != http.StatusOK {
		t.Fatalf("expected tags page to load, got %d", recorder.Code)
	}

	if !strings.Contains(recorder.Body.String(), "Windows Stable") {
		t.Fatal("expected tags page to list the created tag")
	}
}

func loginAsAdmin(t *testing.T, router http.Handler) *http.Cookie {
	t.Helper()

	csrfCookie := ensureCSRFCookie(t, router)
	recorder := performRequest(t, router, http.MethodPost, "/admin/login", url.Values{
		"email":      {"admin@example.com"},
		"password":   {"correct horse battery staple"},
		"next":       {"/admin"},
		"csrf_token": {csrfCookie.Value},
	}, csrfCookie)

	if recorder.Code != http.StatusSeeOther {
		t.Fatalf("expected login redirect, got %d", recorder.Code)
	}

	for _, cookie := range recorder.Result().Cookies() {
		if cookie.Name == "update_server_session" {
			return cookie
		}
	}

	t.Fatal("expected session cookie after login")
	return nil
}

func submitForm(t *testing.T, router http.Handler, method, target string, form url.Values, expectedStatus int, cookies ...*http.Cookie) string {
	t.Helper()

	if strings.HasPrefix(target, "/admin/") {
		csrfCookie := ensureCSRFCookie(t, router, cookies...)
		cookies = upsertCookie(cookies, csrfCookie)
		if form == nil {
			form = url.Values{}
		}
		if form.Get("csrf_token") == "" {
			form.Set("csrf_token", csrfCookie.Value)
		}
	}

	recorder := performRequest(t, router, method, target, form, cookies...)
	if recorder.Code != expectedStatus {
		t.Fatalf("expected %d for %s %s, got %d with body %s", expectedStatus, method, target, recorder.Code, recorder.Body.String())
	}

	return recorder.Header().Get("Location")
}

func submitMultipartForm(t *testing.T, router http.Handler, target string, fields map[string]string, fileField, filename string, body []byte, cookies ...*http.Cookie) string {
	t.Helper()

	if strings.HasPrefix(target, "/admin/") {
		csrfCookie := ensureCSRFCookie(t, router, cookies...)
		cookies = upsertCookie(cookies, csrfCookie)
		if fields == nil {
			fields = map[string]string{}
		}
		if fields["csrf_token"] == "" {
			fields["csrf_token"] = csrfCookie.Value
		}
	}

	var requestBody bytes.Buffer
	writer := multipart.NewWriter(&requestBody)
	for key, value := range fields {
		if err := writer.WriteField(key, value); err != nil {
			t.Fatalf("write multipart field %s: %v", key, err)
		}
	}

	part, err := writer.CreateFormFile(fileField, filename)
	if err != nil {
		t.Fatalf("create multipart file part: %v", err)
	}

	if _, err := part.Write(body); err != nil {
		t.Fatalf("write multipart file body: %v", err)
	}

	if err := writer.Close(); err != nil {
		t.Fatalf("close multipart writer: %v", err)
	}

	req := httptest.NewRequest(http.MethodPost, target, &requestBody)
	req.RemoteAddr = "127.0.0.1:12345"
	req.Header.Set("Content-Type", writer.FormDataContentType())
	for _, cookie := range cookies {
		req.AddCookie(cookie)
	}

	recorder := httptest.NewRecorder()
	router.ServeHTTP(recorder, req)
	if recorder.Code != http.StatusSeeOther {
		t.Fatalf("expected multipart upload redirect, got %d with body %s", recorder.Code, recorder.Body.String())
	}

	return recorder.Header().Get("Location")
}

func upsertCookie(cookies []*http.Cookie, cookie *http.Cookie) []*http.Cookie {
	if cookie == nil {
		return cookies
	}

	updated := make([]*http.Cookie, 0, len(cookies)+1)
	replaced := false
	for _, existing := range cookies {
		if existing == nil {
			continue
		}

		if existing.Name == cookie.Name {
			updated = append(updated, cookie)
			replaced = true
			continue
		}

		updated = append(updated, existing)
	}

	if !replaced {
		updated = append(updated, cookie)
	}

	return updated
}

func extractResourceID(t *testing.T, location, prefix string) string {
	t.Helper()

	if !strings.HasPrefix(location, prefix) {
		t.Fatalf("expected redirect location with prefix %q, got %q", prefix, location)
	}

	trimmed := strings.TrimPrefix(location, prefix)
	parts := strings.SplitN(trimmed, "?", 2)
	if parts[0] == "" {
		t.Fatalf("could not extract resource id from %q", location)
	}

	return parts[0]
}

func mustParseInt64(t *testing.T, raw string) int64 {
	t.Helper()

	value, err := url.PathUnescape(raw)
	if err != nil {
		t.Fatalf("unescape id %q: %v", raw, err)
	}

	var parsed int64
	if _, err := fmt.Sscan(value, &parsed); err != nil {
		t.Fatalf("parse id %q: %v", raw, err)
	}

	return parsed
}
