package httpserver_test

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"testing"
	"time"

	"update_server/internal/apikeys"
	"update_server/internal/db"
)

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

	router, _, store := newTestRouterWithStore(t)
	sessionCookie := loginAsAdmin(t, router)

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

	mobileProjectID := extractResourceID(t, submitForm(t, router, http.MethodPost, "/admin/projects", url.Values{
		"name": {"Mobile App"},
		"slug": {"mobile-app"},
	}, http.StatusSeeOther, sessionCookie), "/admin/projects/")

	legacyProjectID := extractResourceID(t, submitForm(t, router, http.MethodPost, "/admin/projects", url.Values{
		"name": {"Legacy App"},
		"slug": {"legacy-app"},
	}, http.StatusSeeOther, sessionCookie), "/admin/projects/")

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

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

	submitMultipartForm(t, router, "/admin/projects/"+desktopProjectID+"/releases", map[string]string{
		"version":       "1.0.0",
		"build":         "build-1",
		"release_notes": "Initial desktop rollout.",
	}, "artifact", "Desktop-App-1.0.0.zip", []byte("desktop-release-1.0.0"), sessionCookie)

	submitMultipartForm(t, router, "/admin/projects/"+desktopProjectID+"/releases", map[string]string{
		"version":       "1.1.0",
		"build":         "build-2",
		"release_notes": "Improved desktop rollout.",
	}, "artifact", "Desktop-App-1.1.0.zip", []byte("desktop-release-1.1.0"), sessionCookie)

	submitMultipartForm(t, router, "/admin/projects/"+mobileProjectID+"/releases", map[string]string{
		"version":       "2.0.0",
		"build":         "mobile-1",
		"release_notes": "Mobile rollout.",
	}, "artifact", "mobile-app-2.0.0.apk", []byte("mobile-release-2.0.0"), sessionCookie)

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

	keyResult, err := apikeys.NewService(store).Create(t.Context(), apikeys.CreateParams{
		Name:        "Windows Clients",
		ScopeMode:   db.ScopeModeTagAllowList,
		CanDownload: true,
		TagIDs:      []int64{mustParseInt64(t, tagID)},
	})
	if err != nil {
		t.Fatalf("create client api key: %v", err)
	}

	projectsRecorder := performAPIRequest(t, router, http.MethodGet, "/api/v1/projects", "Bearer "+keyResult.RawKey)
	if projectsRecorder.Code != http.StatusOK {
		t.Fatalf("expected project list to return 200, got %d with body %s", projectsRecorder.Code, projectsRecorder.Body.String())
	}

	var projectsPayload struct {
		Projects []struct {
			ID               int64  `json:"id"`
			Name             string `json:"name"`
			Slug             string `json:"slug"`
			LatestReleaseURL string `json:"latest_release_url"`
		} `json:"projects"`
	}
	decodeJSONBody(t, projectsRecorder, &projectsPayload)

	if len(projectsPayload.Projects) != 1 {
		t.Fatalf("expected exactly one accessible active project, got %+v", projectsPayload.Projects)
	}

	if projectsPayload.Projects[0].Slug != "desktop-app" {
		t.Fatalf("expected desktop-app in accessible projects, got %+v", projectsPayload.Projects[0])
	}

	if projectsPayload.Projects[0].LatestReleaseURL != "/api/v1/projects/desktop-app/releases/latest" {
		t.Fatalf("unexpected latest release url %q", projectsPayload.Projects[0].LatestReleaseURL)
	}

	latestRecorder := performAPIRequest(t, router, http.MethodGet, "/api/v1/projects/desktop-app/releases/latest", "Bearer "+keyResult.RawKey)
	if latestRecorder.Code != http.StatusOK {
		t.Fatalf("expected latest release metadata to return 200, got %d with body %s", latestRecorder.Code, latestRecorder.Body.String())
	}

	var latestPayload struct {
		Project struct {
			Slug string `json:"slug"`
		} `json:"project"`
		Release struct {
			ID          int64     `json:"id"`
			Version     string    `json:"version"`
			Build       string    `json:"build"`
			MetadataURL string    `json:"metadata_url"`
			DownloadURL string    `json:"download_url"`
			CreatedAt   time.Time `json:"created_at"`
		} `json:"release"`
	}
	decodeJSONBody(t, latestRecorder, &latestPayload)

	if latestPayload.Project.Slug != "desktop-app" {
		t.Fatalf("expected desktop-app project payload, got %+v", latestPayload.Project)
	}

	if latestPayload.Release.Version != "1.1.0" || latestPayload.Release.Build != "build-2" {
		t.Fatalf("expected latest release 1.1.0/build-2, got %+v", latestPayload.Release)
	}

	if latestPayload.Release.MetadataURL != "/api/v1/releases/2" {
		t.Fatalf("unexpected metadata url %q", latestPayload.Release.MetadataURL)
	}

	if latestPayload.Release.DownloadURL != "/api/v1/releases/2/download" {
		t.Fatalf("unexpected download url %q", latestPayload.Release.DownloadURL)
	}

	metadataRecorder := performAPIRequest(t, router, http.MethodGet, latestPayload.Release.MetadataURL, "Bearer "+keyResult.RawKey)
	if metadataRecorder.Code != http.StatusOK {
		t.Fatalf("expected release metadata to return 200, got %d with body %s", metadataRecorder.Code, metadataRecorder.Body.String())
	}

	var metadataPayload struct {
		Project struct {
			Slug string `json:"slug"`
		} `json:"project"`
		Release struct {
			ID       int64  `json:"id"`
			Version  string `json:"version"`
			Filename string `json:"filename"`
		} `json:"release"`
	}
	decodeJSONBody(t, metadataRecorder, &metadataPayload)

	if metadataPayload.Release.ID != latestPayload.Release.ID || metadataPayload.Release.Version != "1.1.0" {
		t.Fatalf("expected matching release metadata payload, got %+v", metadataPayload.Release)
	}

	blockedLatestRecorder := performAPIRequest(t, router, http.MethodGet, "/api/v1/projects/mobile-app/releases/latest", "Bearer "+keyResult.RawKey)
	if blockedLatestRecorder.Code != http.StatusNotFound {
		t.Fatalf("expected blocked project latest lookup to return 404, got %d with body %s", blockedLatestRecorder.Code, blockedLatestRecorder.Body.String())
	}

	blockedMetadataRecorder := performAPIRequest(t, router, http.MethodGet, "/api/v1/releases/3", "Bearer "+keyResult.RawKey)
	if blockedMetadataRecorder.Code != http.StatusNotFound {
		t.Fatalf("expected blocked release metadata to return 404, got %d with body %s", blockedMetadataRecorder.Code, blockedMetadataRecorder.Body.String())
	}

	downloadRecorder := performAPIRequest(t, router, http.MethodGet, latestPayload.Release.DownloadURL, "Bearer "+keyResult.RawKey)
	if downloadRecorder.Code != http.StatusOK {
		t.Fatalf("expected release download to return 200, got %d with body %s", downloadRecorder.Code, downloadRecorder.Body.String())
	}

	if body := downloadRecorder.Body.String(); body != "desktop-release-1.1.0" {
		t.Fatalf("unexpected download body %q", body)
	}

	if value := downloadRecorder.Header().Get("Content-Disposition"); !strings.Contains(value, "attachment") || !strings.Contains(value, "desktop-app-1.1.0.zip") {
		t.Fatalf("expected attachment content disposition, got %q", value)
	}

	if value := downloadRecorder.Header().Get("Content-Type"); !strings.Contains(value, "text/plain") {
		t.Fatalf("expected sniffed text content type, got %q", value)
	}

	blockedDownloadRecorder := performAPIRequest(t, router, http.MethodGet, "/api/v1/releases/3/download", "Bearer "+keyResult.RawKey)
	if blockedDownloadRecorder.Code != http.StatusNotFound {
		t.Fatalf("expected blocked release download to return 404, got %d with body %s", blockedDownloadRecorder.Code, blockedDownloadRecorder.Body.String())
	}
}

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

	router, _, store := newTestRouterWithStore(t)
	sessionCookie := loginAsAdmin(t, router)

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

	submitMultipartForm(t, router, "/admin/projects/"+projectID+"/releases", map[string]string{
		"version":       "1.0.0",
		"build":         "build-1",
		"release_notes": "Initial rollout.",
	}, "artifact", "desktop-app-1.0.0.zip", []byte("desktop-release-1.0.0"), sessionCookie)

	service := apikeys.NewService(store)

	noPermissionKey, err := service.Create(t.Context(), apikeys.CreateParams{
		Name:      "No Download",
		ScopeMode: db.ScopeModeAllProjects,
		CanUpload: true,
	})
	if err != nil {
		t.Fatalf("create no-permission key: %v", err)
	}

	disabledKey, err := service.Create(t.Context(), apikeys.CreateParams{
		Name:        "Disabled",
		ScopeMode:   db.ScopeModeAllProjects,
		CanDownload: true,
	})
	if err != nil {
		t.Fatalf("create disabled key: %v", err)
	}
	if err := service.SetActive(t.Context(), disabledKey.APIKey.ID, false); err != nil {
		t.Fatalf("disable api key: %v", err)
	}

	expiredAt := time.Now().UTC().Add(-time.Hour)
	expiredKey, err := service.Create(t.Context(), apikeys.CreateParams{
		Name:        "Expired",
		ScopeMode:   db.ScopeModeAllProjects,
		CanDownload: true,
		ExpiresAt:   &expiredAt,
	})
	if err != nil {
		t.Fatalf("create expired key: %v", err)
	}

	missingAuthRecorder := performAPIRequest(t, router, http.MethodGet, "/api/v1/projects", "")
	if missingAuthRecorder.Code != http.StatusUnauthorized {
		t.Fatalf("expected missing auth to return 401, got %d", missingAuthRecorder.Code)
	}
	assertHeaderContains(t, missingAuthRecorder, "WWW-Authenticate", "Bearer")

	noPermissionRecorder := performAPIRequest(t, router, http.MethodGet, "/api/v1/projects", "Bearer "+noPermissionKey.RawKey)
	if noPermissionRecorder.Code != http.StatusForbidden {
		t.Fatalf("expected missing permission to return 403, got %d with body %s", noPermissionRecorder.Code, noPermissionRecorder.Body.String())
	}

	disabledRecorder := performAPIRequest(t, router, http.MethodGet, "/api/v1/projects", "Bearer "+disabledKey.RawKey)
	if disabledRecorder.Code != http.StatusUnauthorized {
		t.Fatalf("expected disabled key to return 401, got %d with body %s", disabledRecorder.Code, disabledRecorder.Body.String())
	}
	assertHeaderContains(t, disabledRecorder, "WWW-Authenticate", "Bearer")

	expiredRecorder := performAPIRequest(t, router, http.MethodGet, "/api/v1/projects", "Bearer "+expiredKey.RawKey)
	if expiredRecorder.Code != http.StatusUnauthorized {
		t.Fatalf("expected expired key to return 401, got %d with body %s", expiredRecorder.Code, expiredRecorder.Body.String())
	}
	assertHeaderContains(t, expiredRecorder, "WWW-Authenticate", "Bearer")
}

func performAPIRequest(t *testing.T, handler http.Handler, method, target, authorization string) *httptest.ResponseRecorder {
	t.Helper()

	req := httptest.NewRequest(method, target, nil)
	req.RemoteAddr = "127.0.0.1:12345"
	if authorization != "" {
		req.Header.Set("Authorization", authorization)
	}

	recorder := httptest.NewRecorder()
	handler.ServeHTTP(recorder, req)
	return recorder
}

func decodeJSONBody(t *testing.T, recorder *httptest.ResponseRecorder, target any) {
	t.Helper()

	if err := json.Unmarshal(recorder.Body.Bytes(), target); err != nil {
		t.Fatalf("decode json response: %v", err)
	}
}
