package main

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

	"github.com/dbledeez/panel/controller/internal/db"
	modulepkg "github.com/dbledeez/panel/pkg/module"
)

func testScaffoldServer(t *testing.T) *httpServer {
	t.Helper()
	return &httpServer{
		log:                slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError})),
		adminToken:         "test-admin-token",
		scaffoldModulesDir: t.TempDir(),
	}
}

func asAdmin(r *http.Request) *http.Request {
	u := &db.UserRow{Role: "admin"}
	return r.WithContext(context.WithValue(r.Context(), sessionUserKey{}, u))
}

func TestTemplateCatalogEndpoint(t *testing.T) {
	h := testScaffoldServer(t)
	r := httptest.NewRequest("GET", "/api/template-catalog", nil)
	w := httptest.NewRecorder()
	h.handleTemplateCatalog(w, r)
	if w.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200", w.Code)
	}
	var entries []map[string]any
	if err := json.Unmarshal(w.Body.Bytes(), &entries); err != nil {
		t.Fatalf("catalog not a JSON array: %v", err)
	}
	if len(entries) < 200 {
		t.Fatalf("catalog has %d entries, expected the full distilled set (>=200)", len(entries))
	}
}

func TestScaffoldModuleForbiddenWithoutAdmin(t *testing.T) {
	h := testScaffoldServer(t)
	body := strings.NewReader(`{"id":"barotrauma"}`)
	r := httptest.NewRequest("POST", "/api/scaffold-module", body)
	w := httptest.NewRecorder()
	h.handleScaffoldModule(w, r)
	if w.Code != http.StatusForbidden {
		t.Fatalf("status = %d, want 403 without admin", w.Code)
	}
	// Non-admin session user is also forbidden.
	r = httptest.NewRequest("POST", "/api/scaffold-module", strings.NewReader(`{"id":"barotrauma"}`))
	r = r.WithContext(context.WithValue(r.Context(), sessionUserKey{}, &db.UserRow{Role: "user"}))
	w = httptest.NewRecorder()
	h.handleScaffoldModule(w, r)
	if w.Code != http.StatusForbidden {
		t.Fatalf("status = %d, want 403 for role=user", w.Code)
	}
}

func TestScaffoldModuleAdminTokenAccepted(t *testing.T) {
	h := testScaffoldServer(t)
	r := httptest.NewRequest("POST", "/api/scaffold-module", strings.NewReader(`{"id":"barotrauma"}`))
	r.Header.Set(adminTokenHeader, "test-admin-token")
	w := httptest.NewRecorder()
	h.handleScaffoldModule(w, r)
	if w.Code != http.StatusCreated {
		t.Fatalf("status = %d, want 201 with valid admin token (body: %s)", w.Code, w.Body.String())
	}
}

// TestScaffoldModuleValidManifest scaffolds a native + a wine entry into a
// temp dir and asserts the generated module.yaml passes pkg/module
// validation, files exist, and the EXPERIMENTAL header is present.
func TestScaffoldModuleValidManifest(t *testing.T) {
	for _, id := range []string{"barotrauma", "abiotic-factor", "arma3"} {
		t.Run(id, func(t *testing.T) {
			h := testScaffoldServer(t)
			r := asAdmin(httptest.NewRequest("POST", "/api/scaffold-module", strings.NewReader(`{"id":"`+id+`"}`)))
			w := httptest.NewRecorder()
			h.handleScaffoldModule(w, r)
			if w.Code != http.StatusCreated {
				t.Fatalf("status = %d, want 201 (body: %s)", w.Code, w.Body.String())
			}
			var resp struct {
				ModuleID string   `json:"module_id"`
				Files    []string `json:"files"`
			}
			if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
				t.Fatalf("bad response json: %v", err)
			}
			if len(resp.Files) != 3 {
				t.Fatalf("files = %v, want module.yaml+Dockerfile+entrypoint.sh", resp.Files)
			}
			dir := filepath.Join(h.scaffoldModulesDir, resp.ModuleID)
			man, err := modulepkg.LoadFile(filepath.Join(dir, "module.yaml"))
			if err != nil {
				raw, _ := os.ReadFile(filepath.Join(dir, "module.yaml"))
				t.Fatalf("scaffolded manifest fails pkg/module validation: %v\n%s", err, raw)
			}
			if man.ID != id {
				t.Errorf("manifest id = %q, want %q", man.ID, id)
			}
			// Every port's env var must be pre-declared in docker.env
			// (agent resolve.go drops undeclared keys).
			for _, p := range man.Ports {
				if p.Env == "" {
					t.Errorf("port %q has no env wiring", p.Name)
					continue
				}
				if _, ok := man.Runtime.Docker.Env[p.Env]; !ok {
					t.Errorf("port env %q not pre-declared in runtime.docker.env", p.Env)
				}
			}
			for _, f := range []string{"module.yaml", "Dockerfile", "entrypoint.sh"} {
				raw, err := os.ReadFile(filepath.Join(dir, f))
				if err != nil {
					t.Fatalf("missing %s: %v", f, err)
				}
				if !strings.Contains(string(raw), "EXPERIMENTAL") {
					t.Errorf("%s missing EXPERIMENTAL header", f)
				}
			}
			// Entrypoint carries the CommandLineArgs hint as a TODO comment
			// and the two-stage setpriv skeleton.
			ep, _ := os.ReadFile(filepath.Join(dir, "entrypoint.sh"))
			if !strings.Contains(string(ep), "setpriv") {
				t.Error("entrypoint missing setpriv stage-1 skeleton")
			}
			// AMP credit in the manifest.
			my, _ := os.ReadFile(filepath.Join(dir, "module.yaml"))
			if !strings.Contains(string(my), "AMPTemplates") {
				t.Error("module.yaml missing AMP template credit")
			}
		})
	}
}

func TestScaffoldModuleRefusesOverwrite(t *testing.T) {
	h := testScaffoldServer(t)
	if err := os.MkdirAll(filepath.Join(h.scaffoldModulesDir, "barotrauma"), 0o755); err != nil {
		t.Fatal(err)
	}
	r := asAdmin(httptest.NewRequest("POST", "/api/scaffold-module", strings.NewReader(`{"id":"barotrauma"}`)))
	w := httptest.NewRecorder()
	h.handleScaffoldModule(w, r)
	if w.Code != http.StatusConflict {
		t.Fatalf("status = %d, want 409 for existing module dir", w.Code)
	}
}

func TestScaffoldModuleUnknownAndBadID(t *testing.T) {
	h := testScaffoldServer(t)
	r := asAdmin(httptest.NewRequest("POST", "/api/scaffold-module", strings.NewReader(`{"id":"no-such-template"}`)))
	w := httptest.NewRecorder()
	h.handleScaffoldModule(w, r)
	if w.Code != http.StatusNotFound {
		t.Fatalf("status = %d, want 404 for unknown template", w.Code)
	}
	r = asAdmin(httptest.NewRequest("POST", "/api/scaffold-module", strings.NewReader(`{"id":"barotrauma","module_id":"../evil"}`)))
	w = httptest.NewRecorder()
	h.handleScaffoldModule(w, r)
	if w.Code != http.StatusBadRequest {
		t.Fatalf("status = %d, want 400 for path-traversal module_id", w.Code)
	}
}
