package main

import (
	"bytes"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	"github.com/go-chi/chi/v5"

	"git.agentview.dev/profit/golangLAKEHOUSE/internal/vectord"
)

// Closes R-005 for vectord: cmd-level tests for the 6 routes.
// Persistence-disabled mode (h.persist == nil) is the test config —
// keeps tests pure-in-memory; persistence is covered by g1p_smoke +
// proof GOLAKE-070.

func mountedRouter() chi.Router {
	h := &handlers{reg: vectord.NewRegistry()}
	r := chi.NewRouter()
	h.register(r)
	return r
}

func TestRoutesMounted(t *testing.T) {
	r := mountedRouter()
	want := map[string]bool{
		"POST /vectors/index":                 false,
		"GET /vectors/index":                  false,
		"GET /vectors/index/{name}":           false,
		"DELETE /vectors/index/{name}":        false,
		"POST /vectors/index/{name}/add":      false,
		"POST /vectors/index/{name}/search":   false,
		"POST /vectors/index/{name}/merge":    false,
	}
	chi.Walk(r, func(method, route string, _ http.Handler, _ ...func(http.Handler) http.Handler) error {
		key := method + " " + route
		if _, ok := want[key]; ok {
			want[key] = true
		}
		return nil
	})
	for sig, found := range want {
		if !found {
			t.Errorf("expected route %q mounted", sig)
		}
	}
}

func TestHandleCreate_HappyPath_201(t *testing.T) {
	r := mountedRouter()
	srv := httptest.NewServer(r)
	defer srv.Close()

	resp, err := http.Post(srv.URL+"/vectors/index", "application/json",
		strings.NewReader(`{"name":"test_idx","dimension":4}`))
	if err != nil {
		t.Fatalf("POST: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusCreated {
		t.Errorf("expected 201 on create, got %d", resp.StatusCode)
	}
}

func TestHandleCreate_MissingDim_400(t *testing.T) {
	r := mountedRouter()
	srv := httptest.NewServer(r)
	defer srv.Close()

	resp, err := http.Post(srv.URL+"/vectors/index", "application/json",
		strings.NewReader(`{"name":"missing_dim"}`))
	if err != nil {
		t.Fatalf("POST: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusBadRequest {
		t.Errorf("expected 400 on missing dim, got %d", resp.StatusCode)
	}
}

func TestHandleCreate_MalformedJSON_400(t *testing.T) {
	r := mountedRouter()
	srv := httptest.NewServer(r)
	defer srv.Close()

	resp, err := http.Post(srv.URL+"/vectors/index", "application/json",
		strings.NewReader("not json"))
	if err != nil {
		t.Fatalf("POST: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusBadRequest {
		t.Errorf("expected 400 on malformed, got %d", resp.StatusCode)
	}
}

func TestHandleCreate_BodyTooLarge(t *testing.T) {
	// 4xx range — see embedd's TestHandleEmbed_BodyTooLarge for the
	// 413-vs-400 unwrap nuance. Contract is "client error, fails loud."
	r := mountedRouter()
	srv := httptest.NewServer(r)
	defer srv.Close()

	big := bytes.Repeat([]byte("x"), maxRequestBytes+(1<<20))
	resp, err := http.Post(srv.URL+"/vectors/index", "application/json", bytes.NewReader(big))
	if err != nil {
		t.Fatalf("POST: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode < 400 || resp.StatusCode >= 500 {
		t.Errorf("expected 4xx on oversize, got %d", resp.StatusCode)
	}
}

func TestHandleGetIndex_NotFound_404(t *testing.T) {
	r := mountedRouter()
	srv := httptest.NewServer(r)
	defer srv.Close()

	resp, err := http.Get(srv.URL + "/vectors/index/nonexistent")
	if err != nil {
		t.Fatalf("GET: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusNotFound {
		t.Errorf("expected 404, got %d", resp.StatusCode)
	}
}

func TestHandleAdd_IndexNotFound_404(t *testing.T) {
	r := mountedRouter()
	srv := httptest.NewServer(r)
	defer srv.Close()

	resp, err := http.Post(srv.URL+"/vectors/index/missing/add", "application/json",
		strings.NewReader(`{"items":[{"id":"v1","vector":[1,2,3,4]}]}`))
	if err != nil {
		t.Fatalf("POST: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusNotFound {
		t.Errorf("expected 404 on add to missing index, got %d", resp.StatusCode)
	}
}

func TestHandleAdd_EmptyItems_400(t *testing.T) {
	r := mountedRouter()
	srv := httptest.NewServer(r)
	defer srv.Close()

	// Create index first.
	http.Post(srv.URL+"/vectors/index", "application/json",
		strings.NewReader(`{"name":"empty_test","dimension":4}`))

	resp, err := http.Post(srv.URL+"/vectors/index/empty_test/add", "application/json",
		strings.NewReader(`{"items":[]}`))
	if err != nil {
		t.Fatalf("POST: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusBadRequest {
		t.Errorf("expected 400 on empty items, got %d", resp.StatusCode)
	}
}

func TestHandleAdd_DimMismatch_400(t *testing.T) {
	r := mountedRouter()
	srv := httptest.NewServer(r)
	defer srv.Close()

	http.Post(srv.URL+"/vectors/index", "application/json",
		strings.NewReader(`{"name":"dim_test","dimension":3}`))

	resp, err := http.Post(srv.URL+"/vectors/index/dim_test/add", "application/json",
		strings.NewReader(`{"items":[{"id":"x","vector":[1,2,3,4]}]}`))
	if err != nil {
		t.Fatalf("POST: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusBadRequest {
		t.Errorf("expected 400 on dim mismatch, got %d", resp.StatusCode)
	}
}

func TestHandleAdd_EmptyID_400(t *testing.T) {
	r := mountedRouter()
	srv := httptest.NewServer(r)
	defer srv.Close()

	http.Post(srv.URL+"/vectors/index", "application/json",
		strings.NewReader(`{"name":"id_test","dimension":4}`))

	resp, err := http.Post(srv.URL+"/vectors/index/id_test/add", "application/json",
		strings.NewReader(`{"items":[{"id":"","vector":[1,2,3,4]}]}`))
	if err != nil {
		t.Fatalf("POST: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusBadRequest {
		t.Errorf("expected 400 on empty id, got %d", resp.StatusCode)
	}
}

func TestHandleSearch_IndexNotFound_404(t *testing.T) {
	r := mountedRouter()
	srv := httptest.NewServer(r)
	defer srv.Close()

	resp, err := http.Post(srv.URL+"/vectors/index/missing/search", "application/json",
		strings.NewReader(`{"vector":[1,2,3,4],"k":5}`))
	if err != nil {
		t.Fatalf("POST: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusNotFound {
		t.Errorf("expected 404 on search of missing index, got %d", resp.StatusCode)
	}
}

func TestHandleDelete_NotFound_404(t *testing.T) {
	r := mountedRouter()
	srv := httptest.NewServer(r)
	defer srv.Close()

	req, _ := http.NewRequest(http.MethodDelete, srv.URL+"/vectors/index/missing", nil)
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatalf("DELETE: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusNotFound {
		t.Errorf("expected 404 deleting missing index, got %d", resp.StatusCode)
	}
}

func TestHandleList_EmptyShape(t *testing.T) {
	r := mountedRouter()
	srv := httptest.NewServer(r)
	defer srv.Close()

	resp, err := http.Get(srv.URL + "/vectors/index")
	if err != nil {
		t.Fatalf("GET: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		t.Errorf("expected 200, got %d", resp.StatusCode)
	}
}

// TestHandleMerge end-to-end via mountedRouter (no external HTTP):
// create source + dest indexes, populate source, merge with
// clear_source=true, assert dest gained the items, source emptied.
// Closes OPEN #1 — locks the merge contract at unit level so a
// future regression on the IDs/Lookup/Add/Delete chain fails here
// before any operator hits "merge again" and silently moves nothing.
func TestHandleMerge_HappyPath_DrainAndClear(t *testing.T) {
	h := &handlers{reg: vectord.NewRegistry()}
	r := chi.NewRouter()
	h.register(r)
	srv := httptest.NewServer(r)
	defer srv.Close()

	// Create both indexes (4-d for test simplicity).
	for _, name := range []string{"fresh_test", "main_test"} {
		body := `{"name":"` + name + `","dimension":4,"distance":"cosine"}`
		resp, err := http.Post(srv.URL+"/vectors/index", "application/json", strings.NewReader(body))
		if err != nil {
			t.Fatalf("create %s: %v", name, err)
		}
		resp.Body.Close()
	}

	// Populate fresh_test with 3 items.
	addBody := `{"items":[
		{"id":"f-1","vector":[1,0,0,0],"metadata":{"name":"fresh-001"}},
		{"id":"f-2","vector":[0,1,0,0],"metadata":{"name":"fresh-002"}},
		{"id":"f-3","vector":[0,0,1,0],"metadata":{"name":"fresh-003"}}
	]}`
	resp, err := http.Post(srv.URL+"/vectors/index/fresh_test/add", "application/json", strings.NewReader(addBody))
	if err != nil || resp.StatusCode != http.StatusOK {
		t.Fatalf("add to fresh_test: status=%d err=%v", resp.StatusCode, err)
	}
	resp.Body.Close()

	// Pre-seed main_test with one item that ALSO exists in fresh
	// (collision) so we exercise the skipped_already_present path.
	preBody := `{"items":[{"id":"f-1","vector":[1,0,0,0],"metadata":{"name":"main-collision"}}]}`
	resp, err = http.Post(srv.URL+"/vectors/index/main_test/add", "application/json", strings.NewReader(preBody))
	if err != nil || resp.StatusCode != http.StatusOK {
		t.Fatalf("add collision to main_test: status=%d err=%v", resp.StatusCode, err)
	}
	resp.Body.Close()

	// Merge fresh_test → main_test, clearing source.
	mergeBody := `{"dest":"main_test","clear_source":true}`
	resp, err = http.Post(srv.URL+"/vectors/index/fresh_test/merge", "application/json", strings.NewReader(mergeBody))
	if err != nil {
		t.Fatalf("merge: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		t.Errorf("expected 200 on merge, got %d", resp.StatusCode)
	}
	var out mergeResponse
	if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
		t.Fatalf("decode merge resp: %v", err)
	}
	if out.Merged != 2 {
		t.Errorf("expected 2 merged (f-2 + f-3), got %d", out.Merged)
	}
	if out.SkippedAlreadyPresent != 1 {
		t.Errorf("expected 1 skipped (f-1 collision), got %d", out.SkippedAlreadyPresent)
	}
	if out.LengthSource != 0 {
		t.Errorf("expected source emptied, got len=%d", out.LengthSource)
	}
	if out.LengthDest != 3 {
		t.Errorf("expected dest len=3 after merge, got %d", out.LengthDest)
	}
}

func TestHandleMerge_DimensionMismatch_400(t *testing.T) {
	h := &handlers{reg: vectord.NewRegistry()}
	r := chi.NewRouter()
	h.register(r)
	srv := httptest.NewServer(r)
	defer srv.Close()

	for _, c := range []struct{ name string; dim int }{
		{"src_4d", 4},
		{"dst_8d", 8},
	} {
		body := `{"name":"` + c.name + `","dimension":` + strconv.Itoa(c.dim) + `,"distance":"cosine"}`
		resp, err := http.Post(srv.URL+"/vectors/index", "application/json", strings.NewReader(body))
		if err != nil {
			t.Fatalf("create %s: %v", c.name, err)
		}
		resp.Body.Close()
	}

	resp, err := http.Post(srv.URL+"/vectors/index/src_4d/merge", "application/json",
		strings.NewReader(`{"dest":"dst_8d"}`))
	if err != nil {
		t.Fatalf("merge: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusBadRequest {
		t.Errorf("expected 400 on dim mismatch, got %d", resp.StatusCode)
	}
}

func TestHandleMerge_DestNotFound_404(t *testing.T) {
	h := &handlers{reg: vectord.NewRegistry()}
	r := chi.NewRouter()
	h.register(r)
	srv := httptest.NewServer(r)
	defer srv.Close()

	body := `{"name":"only_src","dimension":4}`
	resp, err := http.Post(srv.URL+"/vectors/index", "application/json", strings.NewReader(body))
	if err != nil {
		t.Fatalf("create: %v", err)
	}
	resp.Body.Close()

	resp, err = http.Post(srv.URL+"/vectors/index/only_src/merge", "application/json",
		strings.NewReader(`{"dest":"missing_dest"}`))
	if err != nil {
		t.Fatalf("merge: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusNotFound {
		t.Errorf("expected 404 for missing dest, got %d", resp.StatusCode)
	}
}

func TestHandleMerge_SameSourceDest_400(t *testing.T) {
	h := &handlers{reg: vectord.NewRegistry()}
	r := chi.NewRouter()
	h.register(r)
	srv := httptest.NewServer(r)
	defer srv.Close()

	body := `{"name":"self","dimension":4}`
	resp, err := http.Post(srv.URL+"/vectors/index", "application/json", strings.NewReader(body))
	if err != nil {
		t.Fatalf("create: %v", err)
	}
	resp.Body.Close()

	resp, err = http.Post(srv.URL+"/vectors/index/self/merge", "application/json",
		strings.NewReader(`{"dest":"self"}`))
	if err != nil {
		t.Fatalf("merge: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusBadRequest {
		t.Errorf("expected 400 for self-merge, got %d", resp.StatusCode)
	}
}

func TestSearchK_DefaultsAndMax(t *testing.T) {
	if defaultK <= 0 {
		t.Errorf("defaultK = %d, must be > 0", defaultK)
	}
	if maxK < defaultK {
		t.Errorf("maxK=%d < defaultK=%d", maxK, defaultK)
	}
	// Sanity bounds.
	if maxK > 100_000 {
		t.Errorf("maxK=%d unreasonably large", maxK)
	}
}

// TestSaveTask_Coalesces locks the multitier_100k follow-up: a
// burst of triggers must collapse into at most 2 actual saves
// (the in-flight one + one catch-up). Without coalescing, every
// trigger would yield a save and concurrent writers would
// serialize on the index RLock during Encode (the original
// 1-2.5s tail-latency cause).
func TestSaveTask_Coalesces(t *testing.T) {
	var (
		s        saveTask
		saveCnt  atomic.Int32
		started  = make(chan struct{}, 1)
		release  = make(chan struct{})
	)
	save := func() error {
		// First save blocks until released so we can pile up
		// triggers behind it. Subsequent saves return fast so the
		// catch-up logic completes promptly.
		n := saveCnt.Add(1)
		if n == 1 {
			started <- struct{}{}
			<-release
		}
		return nil
	}
	// Trigger first save and wait for it to enter the blocked region.
	s.trigger(save)
	<-started
	// Pile up triggers while the first is blocked. None of these
	// should start their own goroutines — they should mark "pending".
	for i := 0; i < 50; i++ {
		s.trigger(save)
	}
	// Release the first save. The trigger logic should run ONE
	// catch-up save for all 50 piled-up triggers, then return.
	close(release)
	// Wait for the goroutine to drain.
	deadline := time.Now().Add(2 * time.Second)
	for time.Now().Before(deadline) {
		s.mu.Lock()
		idle := !s.inflight && !s.pending
		s.mu.Unlock()
		if idle {
			break
		}
		time.Sleep(5 * time.Millisecond)
	}
	got := saveCnt.Load()
	if got != 2 {
		t.Errorf("save count = %d, want 2 (one in-flight + one catch-up)", got)
	}
}

// TestSaveTask_RunsOnce — single trigger fires exactly one save.
func TestSaveTask_RunsOnce(t *testing.T) {
	var s saveTask
	var n atomic.Int32
	done := make(chan struct{})
	s.trigger(func() error {
		n.Add(1)
		close(done)
		return nil
	})
	select {
	case <-done:
	case <-time.After(2 * time.Second):
		t.Fatal("trigger goroutine never ran")
	}
	// Wait briefly for the goroutine to mark inflight=false.
	time.Sleep(20 * time.Millisecond)
	if got := n.Load(); got != 1 {
		t.Errorf("save count = %d, want 1", got)
	}
}

// TestSaveTask_LogsSaveError — a save error doesn't break the
// coalescing state machine; subsequent triggers still work.
func TestSaveTask_LogsSaveError(t *testing.T) {
	var s saveTask
	var n atomic.Int32
	wantErr := errors.New("boom")
	var wg sync.WaitGroup
	wg.Add(1)
	s.trigger(func() error {
		defer wg.Done()
		n.Add(1)
		return wantErr
	})
	wg.Wait()
	// State must reset so the next trigger fires another save.
	time.Sleep(20 * time.Millisecond)
	wg.Add(1)
	s.trigger(func() error {
		defer wg.Done()
		n.Add(1)
		return nil
	})
	wg.Wait()
	if got := n.Load(); got != 2 {
		t.Errorf("save count = %d, want 2 (failure must not stall the task)", got)
	}
}
