package http

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

	"github.com/denn-gubsky/loomcycle/internal/connector"
	"github.com/denn-gubsky/loomcycle/internal/store"
)

func TestListChannels_ReturnsDeclaredWithZeroCounts(t *testing.T) {
	srv, _, cleanup := systemChannelFixture(t)
	defer cleanup()

	req := authedRequest("GET", "/v1/_channels", nil)
	rec := httptest.NewRecorder()
	srv.Mux().ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String())
	}
	var resp connector.ListChannelsResponse
	if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
		t.Fatalf("decode: %v", err)
	}
	if len(resp.Channels) != 2 {
		t.Fatalf("got %d channels, want 2 (the two declared in fixture): %+v", len(resp.Channels), resp.Channels)
	}
	names := map[string]connector.ChannelDescriptor{}
	for _, c := range resp.Channels {
		names[c.Name] = c
	}
	if d, ok := names["_system/alarms/critical"]; !ok || d.MessageCount != 0 {
		t.Errorf("_system/alarms/critical: %+v", d)
	}
	if d := names["_system/heartbeat-1m"]; d.Period != "1m" || d.Publisher != "system" {
		t.Errorf("heartbeat-1m metadata wrong: %+v", d)
	}
}

func TestListChannels_PopulatesCountsAfterPublish(t *testing.T) {
	srv, s, cleanup := systemChannelFixture(t)
	defer cleanup()

	// Publish two messages to the declared channel via the store
	// directly (the admin POST is exercised elsewhere).
	for i := 0; i < 2; i++ {
		if _, _, err := s.ChannelPublish(t.Context(), store.ChannelMessage{
			Channel: "_system/alarms/critical",
			Scope:   store.MemoryScopeGlobal,
			ScopeID: "global",
			Payload: []byte(`{"severity":"critical"}`),
		}, 1000); err != nil {
			t.Fatalf("ChannelPublish: %v", err)
		}
	}

	req := authedRequest("GET", "/v1/_channels", nil)
	rec := httptest.NewRecorder()
	srv.Mux().ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d, body=%s", rec.Code, rec.Body.String())
	}
	var resp connector.ListChannelsResponse
	if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
		t.Fatalf("decode: %v", err)
	}
	for _, c := range resp.Channels {
		if c.Name == "_system/alarms/critical" {
			if c.MessageCount != 2 {
				t.Errorf("message_count = %d, want 2", c.MessageCount)
			}
			if c.OldestVisibleAt == "" || c.NewestVisibleAt == "" {
				t.Errorf("visible_at bounds should be populated: %+v", c)
			}
			return
		}
	}
	t.Fatal("alarms/critical channel not present in response")
}

// ---- v0.11.5 channel CRUD admin tests -----------------------------

// TestCreateChannel_Runtime_HappyPath creates a new runtime-substrate
// channel via POST /v1/_channels and verifies the descriptor body
// + ListChannels surfaces the row with source="runtime".
func TestCreateChannel_Runtime_HappyPath(t *testing.T) {
	srv, _, cleanup := systemChannelFixture(t)
	defer cleanup()

	body := `{
		"name": "runtime-briefing-ready",
		"description": "Researcher signals editor that a new briefing is ready",
		"scope": "global",
		"semantic": "queue",
		"default_ttl": 3600,
		"max_messages": 100
	}`
	rec := httptest.NewRecorder()
	srv.Mux().ServeHTTP(rec, authedRequest("POST", "/v1/_channels", strings.NewReader(body)))
	if rec.Code != http.StatusCreated {
		t.Fatalf("create status = %d, want 201; body=%s", rec.Code, rec.Body.String())
	}
	var desc connector.ChannelDescriptor
	if err := json.NewDecoder(rec.Body).Decode(&desc); err != nil {
		t.Fatalf("decode: %v", err)
	}
	if desc.Name != "runtime-briefing-ready" {
		t.Errorf("Name = %q", desc.Name)
	}
	if desc.Source != "runtime" {
		t.Errorf("Source = %q, want runtime", desc.Source)
	}
	if desc.DefaultTTL != 3600 || desc.MaxMessages != 100 {
		t.Errorf("ttl/max = %d/%d", desc.DefaultTTL, desc.MaxMessages)
	}

	// List should now surface 3 channels (2 yaml + 1 runtime).
	listRec := httptest.NewRecorder()
	srv.Mux().ServeHTTP(listRec, authedRequest("GET", "/v1/_channels", nil))
	var list connector.ListChannelsResponse
	_ = json.NewDecoder(listRec.Body).Decode(&list)
	bySource := map[string]int{}
	for _, c := range list.Channels {
		bySource[c.Source]++
	}
	if bySource["yaml"] != 2 || bySource["runtime"] != 1 {
		t.Errorf("source distribution = %+v, want yaml:2 runtime:1", bySource)
	}
}

// TestCreateChannel_RejectsOversizedBody is the exp7 regression: admin POST
// bodies must be bounded by http.MaxBytesReader. A valid create body padded
// past the 1 MiB cap with an ignored field decodes fully — and the channel is
// created (201) — on the unfixed code, because the decoder reads the whole
// 2 MiB stream; with the cap the decode trips at 1 MiB and the handler returns
// 400 before the body ever reaches the store. Fail-before: 201 without the cap.
func TestCreateChannel_RejectsOversizedBody(t *testing.T) {
	srv, _, cleanup := systemChannelFixture(t)
	defer cleanup()

	pad := strings.Repeat("x", 2<<20) // 2 MiB, over the 1 MiB cap
	body := `{"name":"oversized-create","scope":"global","semantic":"queue","_pad":"` + pad + `"}`
	rec := httptest.NewRecorder()
	srv.Mux().ServeHTTP(rec, authedRequest("POST", "/v1/_channels", strings.NewReader(body)))

	if rec.Code == http.StatusCreated || rec.Code == http.StatusOK {
		t.Fatalf("oversized create body accepted: status %d — MaxBytesReader missing", rec.Code)
	}
	if rec.Code != http.StatusBadRequest && rec.Code != http.StatusRequestEntityTooLarge {
		t.Errorf("status = %d, want 400 or 413; body=%s", rec.Code, rec.Body.String())
	}
}

// TestCreateChannel_RejectsYamlName refuses with 409
// channel_yaml_immutable so operators can't shadow a yaml channel
// with a runtime row of the same name (yaml is the floor).
func TestCreateChannel_RejectsYamlName(t *testing.T) {
	srv, _, cleanup := systemChannelFixture(t)
	defer cleanup()

	body := `{"name": "_system/alarms/critical", "scope": "global", "semantic": "queue"}`
	rec := httptest.NewRecorder()
	srv.Mux().ServeHTTP(rec, authedRequest("POST", "/v1/_channels", strings.NewReader(body)))
	if rec.Code != http.StatusConflict {
		t.Fatalf("status = %d, want 409; body=%s", rec.Code, rec.Body.String())
	}
	// The yaml name uses a path-segment slash; the validator should
	// also refuse on shape — but the yaml-precedence check fires first
	// in the create flow because the cfg lookup happens before the
	// validChannelName check is meaningful (slash isn't in the
	// allow-set, so the validator would 400 if cfg didn't intercept).
	// Either way the right outcome is a refusal — we just need
	// either code to surface a non-2xx.
	if !strings.Contains(rec.Body.String(), "channel") {
		t.Errorf("body should mention 'channel': %s", rec.Body.String())
	}
}

// TestCreateChannel_RejectsDuplicate creates a runtime row twice;
// the second attempt returns 409 channel_name_in_use.
func TestCreateChannel_RejectsDuplicate(t *testing.T) {
	srv, _, cleanup := systemChannelFixture(t)
	defer cleanup()

	body := `{"name": "dup-test", "scope": "global", "semantic": "queue"}`
	srv.Mux().ServeHTTP(httptest.NewRecorder(),
		authedRequest("POST", "/v1/_channels", strings.NewReader(body)))

	rec := httptest.NewRecorder()
	srv.Mux().ServeHTTP(rec, authedRequest("POST", "/v1/_channels", strings.NewReader(body)))
	if rec.Code != http.StatusConflict {
		t.Fatalf("status = %d, want 409; body=%s", rec.Code, rec.Body.String())
	}
	if !strings.Contains(rec.Body.String(), "channel_name_in_use") {
		t.Errorf("body should mention channel_name_in_use: %s", rec.Body.String())
	}
}

// TestUpdateChannel_HappyPath patches description + max_messages on
// an existing runtime row.
func TestUpdateChannel_HappyPath(t *testing.T) {
	srv, _, cleanup := systemChannelFixture(t)
	defer cleanup()

	create := `{"name": "patch-me", "description": "original", "scope": "global", "semantic": "queue", "max_messages": 10}`
	srv.Mux().ServeHTTP(httptest.NewRecorder(),
		authedRequest("POST", "/v1/_channels", strings.NewReader(create)))

	patch := `{"description": "updated", "max_messages": 500}`
	rec := httptest.NewRecorder()
	srv.Mux().ServeHTTP(rec, authedRequest("PATCH", "/v1/_channels/patch-me", strings.NewReader(patch)))
	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String())
	}
	var desc connector.ChannelDescriptor
	_ = json.NewDecoder(rec.Body).Decode(&desc)
	if desc.Description != "updated" {
		t.Errorf("Description = %q, want updated", desc.Description)
	}
	if desc.MaxMessages != 500 {
		t.Errorf("MaxMessages = %d, want 500", desc.MaxMessages)
	}
}

// TestUpdateChannel_RejectsYamlName refuses PATCH on a yaml-declared
// channel with 409 channel_yaml_immutable.
func TestUpdateChannel_RejectsYamlName(t *testing.T) {
	srv, _, cleanup := systemChannelFixture(t)
	defer cleanup()

	patch := `{"description": "should-not-stick"}`
	rec := httptest.NewRecorder()
	srv.Mux().ServeHTTP(rec, authedRequest("PATCH", "/v1/_channels/_system%2Fheartbeat-1m", strings.NewReader(patch)))
	// Go's mux unescapes %2F before dispatch in the {name} segment;
	// either we get the expected yaml-immutable 409 or a 404 because
	// the path matcher couldn't find a name (the slash kept the route
	// pattern from matching). Both are acceptable refusals; assert
	// non-2xx and that the body says something useful.
	if rec.Code < 400 {
		t.Fatalf("status = %d, want >=400; body=%s", rec.Code, rec.Body.String())
	}
}

// TestUpdateChannel_NotFound returns 404 for a runtime name that
// doesn't exist.
func TestUpdateChannel_NotFound(t *testing.T) {
	srv, _, cleanup := systemChannelFixture(t)
	defer cleanup()

	patch := `{"description": "n/a"}`
	rec := httptest.NewRecorder()
	srv.Mux().ServeHTTP(rec, authedRequest("PATCH", "/v1/_channels/does-not-exist", strings.NewReader(patch)))
	if rec.Code != http.StatusNotFound {
		t.Fatalf("status = %d, want 404; body=%s", rec.Code, rec.Body.String())
	}
}

// TestDeleteChannel_HappyPath deletes a runtime row + 204s; the row
// disappears from List.
func TestDeleteChannel_HappyPath(t *testing.T) {
	srv, _, cleanup := systemChannelFixture(t)
	defer cleanup()

	create := `{"name": "delete-me", "scope": "global", "semantic": "queue"}`
	srv.Mux().ServeHTTP(httptest.NewRecorder(),
		authedRequest("POST", "/v1/_channels", strings.NewReader(create)))

	rec := httptest.NewRecorder()
	srv.Mux().ServeHTTP(rec, authedRequest("DELETE", "/v1/_channels/delete-me", nil))
	if rec.Code != http.StatusNoContent {
		t.Fatalf("status = %d, want 204; body=%s", rec.Code, rec.Body.String())
	}

	listRec := httptest.NewRecorder()
	srv.Mux().ServeHTTP(listRec, authedRequest("GET", "/v1/_channels", nil))
	var list connector.ListChannelsResponse
	_ = json.NewDecoder(listRec.Body).Decode(&list)
	for _, c := range list.Channels {
		if c.Name == "delete-me" {
			t.Errorf("delete-me should be gone but found: %+v", c)
		}
	}
}

// TestDeleteChannel_RejectsYamlName returns 409 channel_yaml_immutable.
func TestDeleteChannel_RejectsYamlName(t *testing.T) {
	srv, _, cleanup := systemChannelFixture(t)
	defer cleanup()

	rec := httptest.NewRecorder()
	srv.Mux().ServeHTTP(rec, authedRequest("DELETE", "/v1/_channels/_system%2Fheartbeat-1m", nil))
	if rec.Code < 400 {
		t.Fatalf("status = %d, want >=400; body=%s", rec.Code, rec.Body.String())
	}
}

// TestCreateChannel_RequiresBearer rejects unauthenticated requests.
func TestCreateChannel_RequiresBearer(t *testing.T) {
	srv, _, cleanup := systemChannelFixture(t)
	defer cleanup()

	body := `{"name": "anon-create", "scope": "global", "semantic": "queue"}`
	req := httptest.NewRequest("POST", "/v1/_channels", strings.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()
	srv.Mux().ServeHTTP(rec, req)
	if rec.Code != http.StatusUnauthorized {
		t.Errorf("status = %d, want 401", rec.Code)
	}
}

func TestListChannels_RequiresBearer(t *testing.T) {
	srv, _, cleanup := systemChannelFixture(t)
	defer cleanup()

	req := httptest.NewRequest("GET", "/v1/_channels", nil)
	// no Authorization header
	rec := httptest.NewRecorder()
	srv.Mux().ServeHTTP(rec, req)

	if rec.Code != http.StatusUnauthorized {
		t.Errorf("status = %d, want 401", rec.Code)
	}
}
