package system

import (
	"encoding/json"
	"net/http"
	"reflect"
	"strings"
	"testing"

	"nadir/internal/oscmd"

	"github.com/danielgtaylor/huma/v2"
	"github.com/danielgtaylor/huma/v2/adapters/humago"
	"github.com/danielgtaylor/huma/v2/humatest"
)

func TestSystemHandlers(t *testing.T) {
	mux := http.NewServeMux()
	api := humatest.Wrap(t, humago.New(mux, huma.DefaultConfig("Test", "1.0.0")))

	registerHostname(api)
	registerTimedate(api)
	registerLocale(api)
	registerPower(api)
	registerInfo(api, NewSampler("/proc/stat", 0))

	// Mock uname for GET /api/system/info
	oscmd.SetMock("uname", func(args []string) oscmd.MockCommand {
		if reflect.DeepEqual(args, []string{"-r"}) {
			return oscmd.MockCommand{Stdout: "6.9.1-1-test\n", ExitCode: 0}
		}
		if reflect.DeepEqual(args, []string{"-m"}) {
			return oscmd.MockCommand{Stdout: "x86_64\n", ExitCode: 0}
		}
		return oscmd.MockCommand{ExitCode: 1}
	})
	// Mock lspci to prevent real calls in case the test host has GPUs.
	oscmd.SetMock("lspci", func(args []string) oscmd.MockCommand {
		return oscmd.MockCommand{ExitCode: 1}
	})
	// Mock nvidia-smi: return failure so enrichment is a no-op.
	oscmd.SetMock("nvidia-smi", func(args []string) oscmd.MockCommand {
		return oscmd.MockCommand{ExitCode: 1}
	})
	defer oscmd.ClearMocks()

	// 1. Test GET /api/system/info
	resp := api.Get("/api/system/info")
	if resp.Code != http.StatusOK {
		t.Errorf("get info: got %d, want %d", resp.Code, http.StatusOK)
	}

	// 2. Test GET & POST /api/system/hostname
	oscmd.SetMock("hostnamectl", func(args []string) oscmd.MockCommand {
		if reflect.DeepEqual(args, []string{"hostname"}) {
			return oscmd.MockCommand{Stdout: "server01\n", ExitCode: 0}
		}
		if reflect.DeepEqual(args, []string{"set-hostname", "--", "server02"}) {
			return oscmd.MockCommand{ExitCode: 0}
		}
		return oscmd.MockCommand{ExitCode: 1}
	})

	resp = api.Get("/api/system/hostname")
	if resp.Code != http.StatusOK {
		t.Errorf("get hostname: got %d, want %d", resp.Code, http.StatusOK)
	}
	var hostnameRes GetHostnameOutput
	if err := json.Unmarshal(resp.Body.Bytes(), &hostnameRes.Body); err != nil {
		t.Fatal(err)
	}
	if hostnameRes.Body.Hostname != "server01" {
		t.Errorf("got hostname %q, want %q", hostnameRes.Body.Hostname, "server01")
	}

	resp = api.Post("/api/system/hostname", struct {
		Hostname string `json:"hostname"`
	}{
		Hostname: "server02",
	})
	if resp.Code != http.StatusOK {
		t.Errorf("set hostname: got %d, want %d", resp.Code, http.StatusOK)
	}

	// 3. Test GET & POST /api/system/time
	oscmd.SetMock("timedatectl", func(args []string) oscmd.MockCommand {
		if reflect.DeepEqual(args, []string{"show"}) {
			showOut := "Timezone=Europe/Rome\nLocalRTC=no\nNTP=yes\nNTPSynchronized=yes\nCanNTP=yes\n"
			return oscmd.MockCommand{Stdout: showOut, ExitCode: 0}
		}
		if reflect.DeepEqual(args, []string{"list-timezones"}) {
			return oscmd.MockCommand{Stdout: "Europe/Rome\nUTC\n", ExitCode: 0}
		}
		if reflect.DeepEqual(args, []string{"set-timezone", "Europe/Rome"}) {
			return oscmd.MockCommand{ExitCode: 0}
		}
		if reflect.DeepEqual(args, []string{"set-ntp", "true"}) {
			return oscmd.MockCommand{ExitCode: 0}
		}
		if len(args) == 2 && args[0] == "set-time" {
			return oscmd.MockCommand{ExitCode: 0}
		}
		return oscmd.MockCommand{ExitCode: 1}
	})

	resp = api.Get("/api/system/time")
	if resp.Code != http.StatusOK {
		t.Errorf("get time: got %d, want %d", resp.Code, http.StatusOK)
	}
	var timeRes GetTimeOutput
	if err := json.Unmarshal(resp.Body.Bytes(), &timeRes.Body); err != nil {
		t.Fatal(err)
	}
	if timeRes.Body.Timezone != "Europe/Rome" || !timeRes.Body.NTP {
		t.Errorf("got time settings: %+v", timeRes.Body)
	}

	resp = api.Get("/api/system/timezones")
	if resp.Code != http.StatusOK {
		t.Errorf("list timezones: got %d, want %d", resp.Code, http.StatusOK)
	}

	resp = api.Post("/api/system/timezone", struct {
		Timezone string `json:"timezone"`
	}{
		Timezone: "Europe/Rome",
	})
	if resp.Code != http.StatusOK {
		t.Errorf("set timezone: got %d, want %d", resp.Code, http.StatusOK)
	}

	resp = api.Post("/api/system/ntp", struct {
		Enabled bool `json:"enabled"`
	}{
		Enabled: true,
	})
	if resp.Code != http.StatusOK {
		t.Errorf("set ntp: got %d, want %d", resp.Code, http.StatusOK)
	}

	resp = api.Post("/api/system/time", struct {
		Time string `json:"time"`
	}{
		Time: "2026-06-20T12:00:00Z",
	})
	if resp.Code != http.StatusOK {
		t.Errorf("set time: got %d, want %d", resp.Code, http.StatusOK)
	}

	// 4. Test GET & POST /api/system/locale
	oscmd.SetMock("localectl", func(args []string) oscmd.MockCommand {
		if reflect.DeepEqual(args, []string{"status"}) {
			statusOut := "   System Locale: LANG=it_IT.UTF-8\n                  LANGUAGE=en_US:\n       VC Keymap: it\n      X11 Layout: it\n"
			return oscmd.MockCommand{Stdout: statusOut, ExitCode: 0}
		}
		if reflect.DeepEqual(args, []string{"list-locales"}) {
			return oscmd.MockCommand{Stdout: "it_IT.UTF-8\nen_US.UTF-8\n", ExitCode: 0}
		}
		if reflect.DeepEqual(args, []string{"set-locale", "LANG=it_IT.UTF-8"}) {
			return oscmd.MockCommand{ExitCode: 0}
		}
		if reflect.DeepEqual(args, []string{"set-locale", "LANG=it_IT.UTF-8", "LANGUAGE=en_US:"}) {
			return oscmd.MockCommand{ExitCode: 0}
		}
		if reflect.DeepEqual(args, []string{"list-keymaps"}) {
			return oscmd.MockCommand{Stdout: "it\nus\n", ExitCode: 0}
		}
		if reflect.DeepEqual(args, []string{"set-keymap", "it"}) {
			return oscmd.MockCommand{ExitCode: 0}
		}
		return oscmd.MockCommand{ExitCode: 1}
	})

	resp = api.Get("/api/system/locale")
	if resp.Code != http.StatusOK {
		t.Errorf("get locale: got %d, want %d", resp.Code, http.StatusOK)
	}
	var localeRes GetLocaleOutput
	if err := json.Unmarshal(resp.Body.Bytes(), &localeRes.Body); err != nil {
		t.Fatal(err)
	}
	if localeRes.Body.Lang != "it_IT.UTF-8" || localeRes.Body.Language != "en_US:" || localeRes.Body.VCKeymap != "it" {
		t.Errorf("got locale status: %+v", localeRes.Body)
	}

	resp = api.Get("/api/system/locales")
	if resp.Code != http.StatusOK {
		t.Errorf("list locales: got %d, want %d", resp.Code, http.StatusOK)
	}

	resp = api.Post("/api/system/locale", struct {
		Lang string `json:"lang"`
	}{
		Lang: "it_IT.UTF-8",
	})
	if resp.Code != http.StatusOK {
		t.Errorf("set locale: got %d, want %d", resp.Code, http.StatusOK)
	}

	resp = api.Post("/api/system/locale", struct {
		Lang     string `json:"lang"`
		Language string `json:"language"`
	}{
		Lang:     "it_IT.UTF-8",
		Language: "en_US:",
	})
	if resp.Code != http.StatusOK {
		t.Errorf("set locale with language: got %d, want %d", resp.Code, http.StatusOK)
	}

	resp = api.Get("/api/system/keymaps")
	if resp.Code != http.StatusOK {
		t.Errorf("list keymaps: got %d, want %d", resp.Code, http.StatusOK)
	}

	resp = api.Post("/api/system/keymap", struct {
		Keymap string `json:"keymap"`
	}{
		Keymap: "it",
	})
	if resp.Code != http.StatusOK {
		t.Errorf("set keymap: got %d, want %d", resp.Code, http.StatusOK)
	}

	// 4b. Test POST /api/system/locale/generate (validation & idempotent)
	// Empty locale → 422 (huma validates non-empty before handler runs)
	resp = api.Post("/api/system/locale/generate", struct {
		Locale string `json:"locale"`
	}{
		Locale: "",
	})
	if resp.Code != http.StatusBadRequest && resp.Code != http.StatusUnprocessableEntity {
		t.Errorf("generate empty locale: got %d, want 400 or 422", resp.Code)
	}

	// Invalid format → 400
	resp = api.Post("/api/system/locale/generate", struct {
		Locale string `json:"locale"`
	}{
		Locale: "not-a-locale!!",
	})
	if resp.Code != http.StatusBadRequest {
		t.Errorf("generate invalid locale: got %d, want %d", resp.Code, http.StatusBadRequest)
	}

	// Already generated (it_IT.UTF-8 is in list-locales mock) → 200 (idempotent)
	resp = api.Post("/api/system/locale/generate", struct {
		Locale string `json:"locale"`
	}{
		Locale: "it_IT.UTF-8",
	})
	if resp.Code != http.StatusOK {
		t.Errorf("generate existing locale (idempotent): got %d, want %d", resp.Code, http.StatusOK)
	}

	// 5. Test POST /api/system/reboot and /api/system/poweroff
	oscmd.SetMock("shutdown", func(args []string) oscmd.MockCommand {
		if reflect.DeepEqual(args, []string{"-r", "now"}) || reflect.DeepEqual(args, []string{"-h", "now"}) {
			return oscmd.MockCommand{ExitCode: 0}
		}
		return oscmd.MockCommand{ExitCode: 1}
	})

	resp = api.Post("/api/system/reboot", struct {
		When string `json:"when"`
	}{
		When: "now",
	})
	if resp.Code != http.StatusOK {
		t.Errorf("reboot: got %d, want %d", resp.Code, http.StatusOK)
	}

	resp = api.Post("/api/system/poweroff", struct {
		When string `json:"when"`
	}{
		When: "now",
	})
	if resp.Code != http.StatusOK {
		t.Errorf("poweroff: got %d, want %d", resp.Code, http.StatusOK)
	}
}

func TestUncommentLocaleGen(t *testing.T) {
	const sampleLocaleGen = `# This file lists locales that you wish to have built.
#
# en_US.UTF-8 UTF-8
# fr_FR.UTF-8 UTF-8
# de_DE.UTF-8 UTF-8
it_IT.UTF-8 UTF-8
`

	tests := []struct {
		name       string
		locale     string
		wantFound  bool
		wantSubstr string // substring that should appear uncommented
	}{
		{
			name:       "uncomment commented locale",
			locale:     "fr_FR.UTF-8",
			wantFound:  true,
			wantSubstr: "\nfr_FR.UTF-8 UTF-8\n",
		},
		{
			name:      "already uncommented",
			locale:    "it_IT.UTF-8",
			wantFound: true,
		},
		{
			name:      "locale not in file",
			locale:    "ja_JP.UTF-8",
			wantFound: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, found := uncommentLocaleGen(sampleLocaleGen, tt.locale)
			if found != tt.wantFound {
				t.Errorf("found = %v, want %v", found, tt.wantFound)
			}
			if tt.wantSubstr != "" && !contains(result, tt.wantSubstr) {
				t.Errorf("result does not contain %q:\n%s", tt.wantSubstr, result)
			}
			// The commented versions of OTHER locales should remain commented.
			if tt.locale == "fr_FR.UTF-8" && !contains(result, "# en_US.UTF-8 UTF-8") {
				t.Errorf("other locales should stay commented:\n%s", result)
			}
		})
	}
}

func contains(s, substr string) bool {
	return len(s) >= len(substr) && (s == substr || len(substr) == 0 ||
		(len(s) > 0 && strings.Contains(s, substr)))
}
