package server

import (
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/uopi/uopi/internal/access"
)

// -------------------------------------------------------------------------- //
// accessMiddleware                                                             //
// -------------------------------------------------------------------------- //

// okHandler records that it ran and returns 200.
func okHandler(ran *bool) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		*ran = true
		w.WriteHeader(http.StatusOK)
	})
}

func TestAccessMiddlewareWriteUserPassesMutation(t *testing.T) {
	// A configured policy with an operator (write) user.
	policy := access.New("", []access.GroupSpec{
		{Name: "ops", Members: map[string]access.Role{"alice": access.RoleOperator}},
	})
	var ran bool
	h := accessMiddleware(policy, testUserHeader, okHandler(&ran))

	req := httptest.NewRequest(http.MethodPost, "/api/v1/interfaces", nil)
	req.Header.Set(testUserHeader, "alice")
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK || !ran {
		t.Errorf("write user POST: code=%d ran=%v, want 200/true", rec.Code, ran)
	}
}

func TestAccessMiddlewareReadUserBlockedFromMutation(t *testing.T) {
	// Configured policy → an unlisted user is a read-only viewer.
	policy := access.New("", []access.GroupSpec{
		{Name: "ops", Members: map[string]access.Role{"alice": access.RoleOperator}},
	})
	var ran bool
	h := accessMiddleware(policy, testUserHeader, okHandler(&ran))

	req := httptest.NewRequest(http.MethodPost, "/api/v1/interfaces", nil)
	req.Header.Set(testUserHeader, "bob") // viewer
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)

	if rec.Code != http.StatusForbidden {
		t.Errorf("read-only POST: code=%d, want 403", rec.Code)
	}
	if ran {
		t.Error("handler must not run for a forbidden mutation")
	}
}

func TestAccessMiddlewareReadUserAllowedToGet(t *testing.T) {
	policy := access.New("", []access.GroupSpec{
		{Name: "ops", Members: map[string]access.Role{"alice": access.RoleOperator}},
	})
	var ran bool
	h := accessMiddleware(policy, testUserHeader, okHandler(&ran))

	req := httptest.NewRequest(http.MethodGet, "/api/v1/interfaces", nil)
	req.Header.Set(testUserHeader, "bob") // viewer
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK || !ran {
		t.Errorf("read-only GET: code=%d ran=%v, want 200/true", rec.Code, ran)
	}
}

func TestAccessMiddlewareMeAlwaysReachable(t *testing.T) {
	policy := access.New("", []access.GroupSpec{
		{Name: "ops", Members: map[string]access.Role{"alice": access.RoleOperator}},
	})
	var ran bool
	h := accessMiddleware(policy, testUserHeader, okHandler(&ran))

	// Even a mutating method on /me is allowed through (identity discovery).
	req := httptest.NewRequest(http.MethodPost, "/api/v1/me", nil)
	req.Header.Set(testUserHeader, "bob")
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK || !ran {
		t.Errorf("/me must always be reachable: code=%d ran=%v", rec.Code, ran)
	}
}

func TestAccessMiddlewareStoresUserOnContext(t *testing.T) {
	policy := access.New("", nil) // unconfigured → everyone write
	var got string
	h := accessMiddleware(policy, testUserHeader, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		got = access.UserFrom(r.Context())
	}))

	req := httptest.NewRequest(http.MethodGet, "/api/v1/foo", nil)
	req.Header.Set(testUserHeader, "carol")
	h.ServeHTTP(httptest.NewRecorder(), req)

	if got != "carol" {
		t.Errorf("context user = %q, want carol", got)
	}
}

// -------------------------------------------------------------------------- //
// httpsRedirectHandler                                                         //
// -------------------------------------------------------------------------- //

func TestHTTPSRedirectHandler(t *testing.T) {
	cases := []struct {
		name    string
		tlsAddr string
		host    string
		target  string
		want    string
	}{
		{"default port omitted", ":443", "example.com", "/panels?x=1", "https://example.com/panels?x=1"},
		{"custom port preserved", ":8443", "host.local:80", "/a", "https://host.local:8443/a"},
		{"root path", "0.0.0.0:443", "h", "/", "https://h/"},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			h := httpsRedirectHandler(c.tlsAddr)
			req := httptest.NewRequest(http.MethodGet, c.target, nil)
			req.Host = c.host
			rec := httptest.NewRecorder()
			h.ServeHTTP(rec, req)

			if rec.Code != http.StatusMovedPermanently {
				t.Fatalf("code = %d, want 301", rec.Code)
			}
			if loc := rec.Header().Get("Location"); loc != c.want {
				t.Errorf("Location = %q, want %q", loc, c.want)
			}
		})
	}
}

// -------------------------------------------------------------------------- //
// parseDialogTarget                                                            //
// -------------------------------------------------------------------------- //

func TestParseDialogTarget(t *testing.T) {
	cases := []struct {
		in       string
		ds, name string
		ok       bool
	}{
		{"epics:SR:CURRENT", "epics", "SR:CURRENT", true}, // splits on first ':' only
		{"bareName", "srv", "bareName", true},             // defaults to srv
		{"  spaced  ", "srv", "spaced", true},
		{"", "", "", false},
		{"   ", "", "", false},
	}
	for _, c := range cases {
		ds, name, ok := parseDialogTarget(c.in)
		if ds != c.ds || name != c.name || ok != c.ok {
			t.Errorf("parseDialogTarget(%q) = (%q,%q,%v), want (%q,%q,%v)",
				c.in, ds, name, ok, c.ds, c.name, c.ok)
		}
	}
}

// -------------------------------------------------------------------------- //
// formatAuditValue                                                             //
// -------------------------------------------------------------------------- //

func TestFormatAuditValue(t *testing.T) {
	cases := []struct {
		in   any
		want string
	}{
		{"hello", "hello"},
		{float64(3.5), "3.5"},
		{float64(42), "42"},
		{true, "true"},
		{false, "false"},
		{nil, ""},
		{[]any{1.0, 2.0}, "[1,2]"}, // default → JSON
		{map[string]any{"a": 1.0}, `{"a":1}`},
	}
	for _, c := range cases {
		if got := formatAuditValue(c.in); got != c.want {
			t.Errorf("formatAuditValue(%v) = %q, want %q", c.in, got, c.want)
		}
	}
}

// -------------------------------------------------------------------------- //
// clientIP                                                                     //
// -------------------------------------------------------------------------- //

func TestClientIP(t *testing.T) {
	t.Run("X-Forwarded-For first hop", func(t *testing.T) {
		req := httptest.NewRequest(http.MethodGet, "/", nil)
		req.Header.Set("X-Forwarded-For", "203.0.113.7, 10.0.0.1")
		if got := clientIP(req); got != "203.0.113.7" {
			t.Errorf("got %q, want 203.0.113.7", got)
		}
	})
	t.Run("X-Real-IP", func(t *testing.T) {
		req := httptest.NewRequest(http.MethodGet, "/", nil)
		req.Header.Set("X-Real-IP", "198.51.100.4")
		if got := clientIP(req); got != "198.51.100.4" {
			t.Errorf("got %q, want 198.51.100.4", got)
		}
	})
	t.Run("RemoteAddr fallback", func(t *testing.T) {
		req := httptest.NewRequest(http.MethodGet, "/", nil)
		req.RemoteAddr = "192.0.2.9:54321"
		if got := clientIP(req); got != "192.0.2.9" {
			t.Errorf("got %q, want 192.0.2.9", got)
		}
	})
}
