package config

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"software-download-center/utils"

	"github.com/gin-gonic/gin"
)

func TestUpdateInfoContainsOnlyFullInstallerAndMSIX(t *testing.T) {
	gin.SetMode(gin.TestMode)
	rootDir := t.TempDir()
	publicDir := filepath.Join(rootDir, "public")
	downloadsDir := filepath.Join(publicDir, "downloads")
	viewsDir := filepath.Join(rootDir, "views")
	mustMkdirAll(t, downloadsDir)
	mustMkdirAll(t, viewsDir)
	writeTestFile(t, downloadsDir, "YMhut_Box_WinUI_Setup_2.0.7.0.exe", "installer")
	writeTestFile(t, downloadsDir, "YMhut_Box_WinUI_2.0.7.0_x64.msix", "msix")
	writeTestFile(t, downloadsDir, "winui.appinstaller", "appinstaller")
	writeTestFile(t, downloadsDir, "YMhut_Box_WinUI_Setup_2.0.7.0_Light.exe", "light")
	writeTestFile(t, filepath.Join(downloadsDir, "incremental"), "YMhut_Box_Update_2.0.6.0_to_2.0.7.0.zip", "incremental")
	writeTestFile(t, publicDir, "update-info.json", `{
		"baselineVersion": "2.0.6.0",
		"minIncrementalVersion": "2.0.6.0",
		"lightInstaller": {"fileName":"light.exe"},
		"packages": [{"id":"external-tools"}],
		"incrementals": [{"id":"delta"}],
		"messages": {"static":"kept"}
	}`)
	writeTemplateFiles(t, viewsDir)

	router := buildTestRouter(t, rootDir)

	recorder := httptest.NewRecorder()
	router.ServeHTTP(recorder, httptest.NewRequest("GET", "/update-info.json", nil))
	if recorder.Code != http.StatusOK {
		t.Fatalf("expected update-info 200, got %d: %s", recorder.Code, recorder.Body.String())
	}

	var body map[string]interface{}
	if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil {
		t.Fatal(err)
	}

	assertAbsent(t, body, "baselineVersion", "minIncrementalVersion", "lightInstaller", "packages", "incrementals", "requiredForFull", "modules")
	if body["latestVersion"] != "2.0.7.0" {
		t.Fatalf("expected latestVersion 2.0.7.0, got %#v", body["latestVersion"])
	}

	latest := body["latest"].(map[string]interface{})
	assertAbsent(t, latest, "lightInstaller", "incrementals", "packages")
	fullInstaller := latest["fullInstaller"].(map[string]interface{})
	if fullInstaller["fileName"] != "YMhut_Box_WinUI_Setup_2.0.7.0.exe" {
		t.Fatalf("unexpected full installer: %#v", fullInstaller)
	}
	msix := latest["msix"].(map[string]interface{})
	if msix["fileName"] != "YMhut_Box_WinUI_2.0.7.0_x64.msix" {
		t.Fatalf("unexpected msix: %#v", msix)
	}
	appInstaller := latest["appInstaller"].(map[string]interface{})
	if appInstaller["fileName"] != "winui.appinstaller" {
		t.Fatalf("unexpected appinstaller: %#v", appInstaller)
	}

	text := recorder.Body.String()
	for _, forbidden := range []string{"_Light.exe", "YMhut_Box_Update_", "external-tools", "baselineVersion", "incrementals"} {
		if strings.Contains(text, forbidden) {
			t.Fatalf("update-info leaked removed field/content %q: %s", forbidden, text)
		}
	}
}

func TestRemovedDistributionRoutesReturnGone(t *testing.T) {
	gin.SetMode(gin.TestMode)
	rootDir := t.TempDir()
	publicDir := filepath.Join(rootDir, "public")
	downloadsDir := filepath.Join(publicDir, "downloads")
	viewsDir := filepath.Join(rootDir, "views")
	mustMkdirAll(t, downloadsDir)
	mustMkdirAll(t, viewsDir)
	writeTemplateFiles(t, viewsDir)

	router := buildTestRouter(t, rootDir)

	for _, route := range []string{
		"/modules.json",
		"/api/modules",
		"/package-manifest.json",
		"/incremental-manifest.json",
		"/api/packages/manifest",
		"/api/incrementals",
		"/api/incrementals/manifest",
		"/api/packages/download/YMhut_Box_Tools_2.0.6.0.zip",
		"/api/incrementals/download/YMhut_Box_Update_2.0.6.0_to_2.0.7.0.zip",
		"/packages/YMhut_Box_Tools_2.0.6.0.zip",
		"/tool-packages/YMhut_Box_Tools_2.0.6.0.zip",
	} {
		recorder := httptest.NewRecorder()
		router.ServeHTTP(recorder, httptest.NewRequest("GET", route, nil))
		if recorder.Code != http.StatusGone {
			t.Fatalf("expected %s to return 410, got %d: %s", route, recorder.Code, recorder.Body.String())
		}
	}
}

func TestCanonicalUpdateInfoRoutesServeDirectlyAndLegacyRoutesRedirect(t *testing.T) {
	gin.SetMode(gin.TestMode)
	rootDir := t.TempDir()
	publicDir := filepath.Join(rootDir, "public")
	downloadsDir := filepath.Join(publicDir, "downloads")
	viewsDir := filepath.Join(rootDir, "views")
	mustMkdirAll(t, downloadsDir)
	mustMkdirAll(t, viewsDir)
	writeTemplateFiles(t, viewsDir)

	router := buildTestRouter(t, rootDir)

	for _, route := range []string{"/update-info.json", "/update-info", "/api/update-info"} {
		recorder := httptest.NewRecorder()
		router.ServeHTTP(recorder, httptest.NewRequest("GET", route, nil))
		if recorder.Code != http.StatusOK {
			t.Fatalf("expected %s to return 200, got %d: %s", route, recorder.Code, recorder.Body.String())
		}
	}

	manifestRecorder := httptest.NewRecorder()
	router.ServeHTTP(manifestRecorder, httptest.NewRequest("GET", "/manifest.json", nil))
	if manifestRecorder.Code != http.StatusOK {
		t.Fatalf("expected /manifest.json compatibility response 200, got %d: %s", manifestRecorder.Code, manifestRecorder.Body.String())
	}
	if manifestRecorder.Header().Get("Deprecation") != "true" {
		t.Fatalf("expected /manifest.json to include Deprecation header")
	}
	if manifestRecorder.Header().Get("X-YMhut-Canonical-Manifest") != "/update-info.json" {
		t.Fatalf("expected /manifest.json to point at /update-info.json")
	}

	for _, route := range []string{"/latest-version.json", "/api/releases/latest", "/latest.json"} {
		recorder := httptest.NewRecorder()
		router.ServeHTTP(recorder, httptest.NewRequest("GET", route, nil))
		if recorder.Code != http.StatusMovedPermanently {
			t.Fatalf("expected %s to redirect with 301, got %d", route, recorder.Code)
		}
		if location := recorder.Header().Get("Location"); location != "/update-info.json" {
			t.Fatalf("expected %s Location /update-info.json, got %q", route, location)
		}
	}
}

func TestDownloadsRouteAllowsOnlySingleInstallerArtifact(t *testing.T) {
	gin.SetMode(gin.TestMode)
	rootDir := t.TempDir()
	publicDir := filepath.Join(rootDir, "public")
	downloadsDir := filepath.Join(publicDir, "downloads")
	viewsDir := filepath.Join(rootDir, "views")
	mustMkdirAll(t, downloadsDir)
	mustMkdirAll(t, viewsDir)
	writeTestFile(t, downloadsDir, "YMhut_Box_WinUI_Setup_2.0.7.0.exe", "installer")
	writeTestFile(t, downloadsDir, "YMhut_Box_WinUI_Setup_2.0.7.0_Light.exe", "light")
	writeTestFile(t, filepath.Join(downloadsDir, "incremental"), "YMhut_Box_Update_2.0.6.0_to_2.0.7.0.zip", "incremental")
	writeTemplateFiles(t, viewsDir)

	router := buildTestRouter(t, rootDir)

	okRecorder := httptest.NewRecorder()
	router.ServeHTTP(okRecorder, httptest.NewRequest("GET", "/downloads/YMhut_Box_WinUI_Setup_2.0.7.0.exe", nil))
	if okRecorder.Code != http.StatusOK {
		t.Fatalf("expected full installer download 200, got %d: %s", okRecorder.Code, okRecorder.Body.String())
	}
	if okRecorder.Body.String() != "installer" {
		t.Fatalf("unexpected installer body: %q", okRecorder.Body.String())
	}

	for _, route := range []string{
		"/downloads/YMhut_Box_WinUI_Setup_2.0.7.0_Light.exe",
		"/downloads/incremental/YMhut_Box_Update_2.0.6.0_to_2.0.7.0.zip",
		"/downloads/..%2Fsecret.exe",
		"/downloads/subdir/file.exe",
	} {
		recorder := httptest.NewRecorder()
		router.ServeHTTP(recorder, httptest.NewRequest("GET", route, nil))
		if recorder.Code != http.StatusForbidden {
			t.Fatalf("expected %s to be forbidden, got %d: %s", route, recorder.Code, recorder.Body.String())
		}
	}
}

func TestAdminAPIRequiresAuthentication(t *testing.T) {
	gin.SetMode(gin.TestMode)
	rootDir := t.TempDir()
	publicDir := filepath.Join(rootDir, "public")
	downloadsDir := filepath.Join(publicDir, "downloads")
	viewsDir := filepath.Join(rootDir, "views")
	mustMkdirAll(t, downloadsDir)
	mustMkdirAll(t, viewsDir)
	writeTemplateFiles(t, viewsDir)

	router := buildTestRouter(t, rootDir)

	recorder := httptest.NewRecorder()
	router.ServeHTTP(recorder, httptest.NewRequest("GET", "/api/admin/releases/files", nil))
	if recorder.Code != http.StatusUnauthorized {
		t.Fatalf("expected 401 for unauthenticated admin API, got %d: %s", recorder.Code, recorder.Body.String())
	}
	if !strings.Contains(recorder.Body.String(), "UNAUTHORIZED") {
		t.Fatalf("expected structured unauthorized response, got %s", recorder.Body.String())
	}
}

func TestAdminPageShowsUnauthorizedShellWithoutAuth(t *testing.T) {
	gin.SetMode(gin.TestMode)
	rootDir := t.TempDir()
	publicDir := filepath.Join(rootDir, "public")
	downloadsDir := filepath.Join(publicDir, "downloads")
	viewsDir := filepath.Join(rootDir, "views")
	mustMkdirAll(t, downloadsDir)
	mustMkdirAll(t, viewsDir)
	writeTemplateFiles(t, viewsDir)

	router := buildTestRouter(t, rootDir)

	recorder := httptest.NewRecorder()
	router.ServeHTTP(recorder, httptest.NewRequest("GET", "/admin/", nil))
	if recorder.Code != http.StatusOK {
		t.Fatalf("expected admin shell 200, got %d: %s", recorder.Code, recorder.Body.String())
	}
	if !strings.Contains(recorder.Body.String(), "未授权") {
		t.Fatalf("expected admin shell fallback to show unauthorized copy, got %s", recorder.Body.String())
	}
}

func TestComparePackageFileNamesSupportsNewAndLegacyInstallerNames(t *testing.T) {
	if comparePackageFileNames("YMhut_Box_WinUI_Setup_2.0.7.0.exe", "YMhut_Box_Setup_2.0.6.0.exe") <= 0 {
		t.Fatal("expected WinUI 2.0.7.0 installer to be newer than legacy 2.0.6.0 installer")
	}
	if comparePackageFileNames("YMhut_Box_Setup_2.0.7.0.exe", "YMhut_Box_WinUI_Setup_2.0.7.0.exe") == 0 {
		t.Fatal("expected stable tie-breaker for same-version installer names")
	}
}

func TestProductsInfoRecognizesMSIXAndSkipsIncrementalSubdirectory(t *testing.T) {
	dir := t.TempDir()
	writeTestFile(t, dir, "YMhut_Box_WinUI_2.0.7.0_x64.msix", "msix")
	writeTestFile(t, filepath.Join(dir, "incremental"), "YMhut_Box_Update_2.0.6.0_to_2.0.7.0.zip", "incremental")

	products := utils.GetProductsInfo(dir, utils.NewLogger())
	if len(products) == 0 {
		t.Fatalf("expected MSIX package to be detected")
	}
	for _, releases := range products {
		for _, release := range releases {
			if release.FileName == "YMhut_Box_Update_2.0.6.0_to_2.0.7.0.zip" {
				t.Fatalf("incremental package from subdirectory should not be listed as product: %#v", products)
			}
		}
	}
}

func buildTestRouter(t *testing.T, rootDir string) *gin.Engine {
	t.Helper()
	oldWorkingDir, err := os.Getwd()
	if err != nil {
		t.Fatal(err)
	}
	if err := os.Chdir(rootDir); err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() {
		_ = os.Chdir(oldWorkingDir)
	})

	router := gin.New()
	RegisterRoutes(router, utils.NewLogger())
	return router
}

func writeTemplateFiles(t *testing.T, dir string) {
	t.Helper()
	writeTestFile(t, dir, "index.html", "{{.pageTitle}}")
	writeTestFile(t, dir, "404.html", "{{.title}}")
	writeTestFile(t, dir, "500.html", "{{.title}}")
}

func writeTestFile(t *testing.T, dir string, name string, content string) {
	t.Helper()
	if err := os.MkdirAll(dir, 0o700); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o600); err != nil {
		t.Fatalf("write %s: %v", name, err)
	}
}

func mustMkdirAll(t *testing.T, dir string) {
	t.Helper()
	if err := os.MkdirAll(dir, 0o700); err != nil {
		t.Fatal(err)
	}
}

func assertAbsent(t *testing.T, body map[string]interface{}, keys ...string) {
	t.Helper()
	for _, key := range keys {
		if _, ok := body[key]; ok {
			t.Fatalf("expected field %s to be absent in %#v", key, body)
		}
	}
}
