package service

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

	"metis/pkg/facts"
	"metis/pkg/sentinel"
)

func TestScratchHealthAnnotations(t *testing.T) {
	if err := newTestApp(t).syncScratchAnnotations(SnapshotRecord{}); err != nil {
		t.Fatalf("nil scratch sync should be a noop: %v", err)
	}
	if status, detail := scratchStatusDetail(nil); status != "missing" || detail != "no-scratch-snapshot" {
		t.Fatalf("nil scratch detail mismatch: %s %s", status, detail)
	}

	observed := time.Date(2026, 4, 22, 6, 45, 0, 0, time.UTC)
	annotations := scratchHealthAnnotations(&facts.USBScratch{
		Mountpoint:   "/mnt/astraios",
		UUID:         "usb-1",
		FS:           "ext4",
		MountHealthy: true,
		UUIDHealthy:  true,
		BindHealthy:  true,
		BindTargets:  []facts.USBBindTarget{{Path: "/var/log/pods", Healthy: true}, {Path: "/var/tmp", Healthy: true}},
	}, observed)
	if annotations["maintenance.bstein.dev/usb-scratch-status"] != "ok" || annotations["maintenance.bstein.dev/astraios-detail"] != "healthy" {
		t.Fatalf("unexpected healthy annotations: %#v", annotations)
	}
	if annotations["maintenance.bstein.dev/usb-scratch-selector"] != "UUID=usb-1" {
		t.Fatalf("selector annotation missing: %#v", annotations)
	}
	if annotations["maintenance.bstein.dev/astraios-managed-paths"] != "/var/log/pods_/var/tmp" {
		t.Fatalf("managed paths annotation mismatch: %#v", annotations)
	}

	labelAnnotations := scratchHealthAnnotations(&facts.USBScratch{
		Mountpoint:   "/mnt/scratch",
		Label:        "scratch-a",
		MountHealthy: true,
		LabelHealthy: true,
		BindHealthy:  true,
	}, observed)
	if labelAnnotations["maintenance.bstein.dev/usb-scratch-selector"] != "LABEL=scratch-a" {
		t.Fatalf("label selector annotation mismatch: %#v", labelAnnotations)
	}

	status, detail := scratchStatusDetail(&facts.USBScratch{Label: "scratch-a", MountHealthy: false, LabelHealthy: false, BindHealthy: false})
	if status != "error" || !strings.Contains(detail, "mount-unhealthy") || !strings.Contains(detail, "label-mismatch") || !strings.Contains(detail, "bind-mount-incomplete") {
		t.Fatalf("unexpected unhealthy detail: %s %s", status, detail)
	}
}

func TestStoreSnapshotPatchesNodeAnnotations(t *testing.T) {
	var patchPath string
	var patchContentType string
	var patchBody map[string]any
	kube := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPatch || r.URL.Path != "/api/v1/nodes/titan-04" {
			http.NotFound(w, r)
			return
		}
		patchPath = r.URL.Path
		patchContentType = r.Header.Get("Content-Type")
		if err := json.NewDecoder(r.Body).Decode(&patchBody); err != nil {
			t.Fatalf("decode patch body: %v", err)
		}
		_ = json.NewEncoder(w).Encode(map[string]any{"status": "ok"})
	}))
	defer kube.Close()

	origFactory := kubeClientFactory
	kubeClientFactory = func() (*kubeClient, error) {
		return kubeClientFactoryForURL(kube.URL, kube.Client()), nil
	}
	t.Cleanup(func() { kubeClientFactory = origFactory })

	app := newTestApp(t)
	if err := app.StoreSnapshot(SnapshotRecord{
		Node:        "titan-04",
		CollectedAt: time.Date(2026, 4, 22, 6, 50, 0, 0, time.UTC),
		Snapshot: sentinel.Snapshot{
			Hostname: "titan-04",
			USBScratch: &facts.USBScratch{
				Mountpoint:   "/mnt/astraios",
				UUID:         "usb-1",
				MountHealthy: true,
				UUIDHealthy:  true,
				BindHealthy:  true,
				BindTargets:  []facts.USBBindTarget{{Path: "/var/log/pods", Healthy: true}},
			},
		},
	}); err != nil {
		t.Fatalf("StoreSnapshot: %v", err)
	}
	if patchPath != "/api/v1/nodes/titan-04" || patchContentType != "application/merge-patch+json" {
		t.Fatalf("patch request mismatch: path=%q content-type=%q", patchPath, patchContentType)
	}
	metadata := patchBody["metadata"].(map[string]any)
	annotations := metadata["annotations"].(map[string]any)
	if annotations["maintenance.bstein.dev/usb-scratch-status"] != "ok" || annotations["maintenance.bstein.dev/astraios-selector"] != "UUID=usb-1" {
		t.Fatalf("annotation patch mismatch: %#v", annotations)
	}
}

func TestStoreSnapshotRecordsAnnotationPatchFailure(t *testing.T) {
	kube := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		http.Error(w, "denied", http.StatusForbidden)
	}))
	defer kube.Close()

	origFactory := kubeClientFactory
	kubeClientFactory = func() (*kubeClient, error) {
		return kubeClientFactoryForURL(kube.URL, kube.Client()), nil
	}
	t.Cleanup(func() { kubeClientFactory = origFactory })

	app := newTestApp(t)
	if err := app.StoreSnapshot(SnapshotRecord{
		Node:        "titan-04",
		CollectedAt: time.Date(2026, 4, 22, 6, 55, 0, 0, time.UTC),
		Snapshot: sentinel.Snapshot{
			Hostname: "titan-04",
			USBScratch: &facts.USBScratch{
				Mountpoint:   "/mnt/astraios",
				MountHealthy: true,
				BindHealthy:  true,
			},
		},
	}); err != nil {
		t.Fatalf("StoreSnapshot should keep the snapshot even if annotation sync fails: %v", err)
	}
	state := app.State("")
	found := false
	for _, event := range state.Events {
		if event.Kind == "sentinel.annotation" {
			found = true
			break
		}
	}
	if !found {
		t.Fatalf("expected annotation warning event, got %#v", state.Events)
	}
}

func TestPatchNodeAnnotationsNoopsAndClientErrors(t *testing.T) {
	if err := patchNodeAnnotations("", map[string]string{"a": "b"}); err != nil {
		t.Fatalf("empty node should be a noop: %v", err)
	}
	if err := patchNodeAnnotations("node", nil); err != nil {
		t.Fatalf("empty annotations should be a noop: %v", err)
	}

	origFactory := kubeClientFactory
	kubeClientFactory = func() (*kubeClient, error) { return nil, errors.New("offline") }
	if err := patchNodeAnnotations("node", map[string]string{"a": "b"}); err == nil || !strings.Contains(err.Error(), "offline") {
		t.Fatalf("expected factory error, got %v", err)
	}
	t.Cleanup(func() { kubeClientFactory = origFactory })

	client := kubeClientFactoryForURL("http://127.0.0.1", &http.Client{Transport: failingRoundTripper{}})
	if err := client.mergePatch("/api/v1/nodes/node", map[string]string{"a": "b"}); err == nil || !strings.Contains(err.Error(), "transport down") {
		t.Fatalf("expected transport error, got %v", err)
	}
	if err := client.mergePatch("/api/v1/nodes/node", map[string]any{"bad": func() {}}); err == nil {
		t.Fatal("expected marshal error")
	}
	badURLClient := kubeClientFactoryForURL("http://[::1", http.DefaultClient)
	if err := badURLClient.mergePatch("/api/v1/nodes/node", map[string]string{"a": "b"}); err == nil {
		t.Fatal("expected request construction error")
	}
}

type failingRoundTripper struct{}

func (failingRoundTripper) RoundTrip(*http.Request) (*http.Response, error) {
	return nil, errors.New("transport down")
}
