package agentapi

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
)

// memProber satisfies the SupportProber contract (NetVerifyStatus) PLUS the memory probe's
// type-asserted GuestMemory + the AgentVersion fast-path — the shape the web memAgent seam has.
type memProber struct {
	memErr error
	ver    string
}

func (p *memProber) NetVerifyStatus(context.Context) (NetVerifyStatus, error) {
	return NetVerifyStatus{Phase: "none"}, nil
}
func (p *memProber) GuestMemory(context.Context) (GuestMemoryInfo, error) {
	return GuestMemoryInfo{AllocatedMB: 8192}, p.memErr
}
func (p *memProber) AgentVersion() string { return p.ver }

// The capability table: v0.90.0 → Yes, v0.89.0 → No via the version fast-path; the probe (no
// version) classifies 404 → No, nil → Yes.
func TestGuestMemory_Capability(t *testing.T) {
	t.Run("version >= 0.90 → Yes", func(t *testing.T) {
		var sc SupportCache
		if got := sc.Supports(context.Background(), &memProber{ver: "0.90.0"}, FeatureGuestMemoryResize); got != SupportYes {
			t.Errorf("v0.90.0 = %v, want SupportYes", got)
		}
	})
	t.Run("version < 0.90 → No", func(t *testing.T) {
		var sc SupportCache
		if got := sc.Supports(context.Background(), &memProber{ver: "0.89.0"}, FeatureGuestMemoryResize); got != SupportNo {
			t.Errorf("v0.89.0 = %v, want SupportNo", got)
		}
	})
	t.Run("no version, probe 404 → No", func(t *testing.T) {
		var sc SupportCache
		p := &memProber{memErr: &StatusError{Path: "/guest/memory", Code: http.StatusNotFound}}
		if got := sc.Supports(context.Background(), p, FeatureGuestMemoryResize); got != SupportNo {
			t.Errorf("probe 404 = %v, want SupportNo", got)
		}
	})
	t.Run("no version, probe ok → Yes", func(t *testing.T) {
		var sc SupportCache
		if got := sc.Supports(context.Background(), &memProber{}, FeatureGuestMemoryResize); got != SupportYes {
			t.Errorf("probe ok = %v, want SupportYes", got)
		}
	})
}

// GuestMemory decodes the agent's payload; a 404 (pre-0.90) is the typed *StatusError.
func TestClient_GuestMemory(t *testing.T) {
	mux := http.NewServeMux()
	mux.HandleFunc("/guest/memory", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodGet {
			w.WriteHeader(405)
			return
		}
		_, _ = w.Write([]byte(`{"ok":true,"data":{"vmid":9201,"allocated_mb":8192,"usage_mb":3000,"host_total_mb":16384,"min_mb":2048,"max_mb":14336,"floor_mb":3512,"running":true}}`))
	})
	c := newMemTestClient(t, mux)
	info, err := c.GuestMemory(context.Background())
	if err != nil {
		t.Fatalf("GuestMemory: %v", err)
	}
	if info.AllocatedMB != 8192 || info.UsageMB != 3000 || info.MaxMB != 14336 || info.FloorMB != 3512 || !info.Running {
		t.Errorf("decoded wrong: %+v", info)
	}
}

func TestClient_GuestMemory_404(t *testing.T) {
	c := newMemTestClient(t, http.NewServeMux()) // no route → 404
	_, err := c.GuestMemory(context.Background())
	var se *StatusError
	if !errors.As(err, &se) || se.Code != http.StatusNotFound {
		t.Fatalf("pre-0.90 agent must 404 as *StatusError, got %T: %v", err, err)
	}
}

// ResizeMemory: success returns old→new; a 412 refusal surfaces *MemoryRefusedError with the code.
func TestClient_ResizeMemory(t *testing.T) {
	t.Run("success", func(t *testing.T) {
		mux := http.NewServeMux()
		mux.HandleFunc("/guest/memory", func(w http.ResponseWriter, r *http.Request) {
			_, _ = w.Write([]byte(`{"ok":true,"data":{"vmid":9201,"old_mb":8192,"new_mb":12288,"unchanged":false}}`))
		})
		c := newMemTestClient(t, mux)
		res, err := c.ResizeMemory(context.Background(), 12288)
		if err != nil {
			t.Fatalf("ResizeMemory: %v", err)
		}
		if res.OldMB != 8192 || res.NewMB != 12288 {
			t.Errorf("result = %+v", res)
		}
	})
	t.Run("refusal carries the code", func(t *testing.T) {
		mux := http.NewServeMux()
		mux.HandleFunc("/guest/memory", func(w http.ResponseWriter, r *http.Request) {
			w.WriteHeader(http.StatusPreconditionFailed)
			_, _ = w.Write([]byte(`{"ok":false,"error":"requested 3300 MB is too close to current usage 3000 MB (floor 3512 MB)","data":{"code":"below_usage_floor","usage_mb":3000,"floor_mb":3512,"min_mb":2048,"max_mb":14336}}`))
		})
		c := newMemTestClient(t, mux)
		_, err := c.ResizeMemory(context.Background(), 3300)
		var refused *MemoryRefusedError
		if !errors.As(err, &refused) {
			t.Fatalf("want *MemoryRefusedError, got %T: %v", err, err)
		}
		if refused.Code != "below_usage_floor" || refused.Bounds.UsageMB != 3000 || refused.Bounds.FloorMB != 3512 {
			t.Errorf("refusal = %+v", refused)
		}
	})
}

func newMemTestClient(t *testing.T, mux *http.ServeMux) *Client {
	t.Helper()
	srv := httptest.NewTLSServer(mux)
	t.Cleanup(srv.Close)
	fp := sha256.Sum256(srv.Certificate().Raw)
	c, err := New(strings.TrimPrefix(srv.URL, "https://"), "test-token", hex.EncodeToString(fp[:]))
	if err != nil {
		t.Fatalf("New: %v", err)
	}
	return c
}
