package adapter

import (
	"encoding/json"
	"io"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"testing"
	"time"
)

func discardLogger() *slog.Logger {
	return slog.New(slog.NewTextHandler(io.Discard, nil))
}

const sampleDispatch = `{
  "alert_id": "ALT-2607-42",
  "run_id": 7,
  "agent": "runbook-triage",
  "trigger_kind": "alert_created",
  "alertname": "UnhealthyPod",
  "classification": "page",
  "cluster_id": "137",
  "tenant_id": "t-99",
  "description": "Pod is unhealthy",
  "disclosure": "shown",
  "callback": {"url": "%s"}
}`

// stubUsher answers POST /api/sessions the way usher does and records what it
// was sent.
type stubUsher struct {
	mu       sync.Mutex
	requests []sessionRequest
	status   int
}

func (s *stubUsher) handler() http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/api/sessions" {
			w.WriteHeader(http.StatusNotFound)
			return
		}
		var req sessionRequest
		_ = json.NewDecoder(r.Body).Decode(&req)
		s.mu.Lock()
		s.requests = append(s.requests, req)
		s.mu.Unlock()

		if s.status != 0 {
			w.WriteHeader(s.status)
			_, _ = w.Write([]byte(`{"error":"nope"}`))
			return
		}
		w.WriteHeader(http.StatusAccepted)
		_, _ = w.Write([]byte(`{"id":"sess-abc123"}`))
	})
}

func newTestAdapter(t *testing.T, usherURL, proxyURL string) *Adapter {
	t.Helper()
	promptPath := filepath.Join(t.TempDir(), "prompt.md")
	body := "alert {{ALERT_ID}} / {{ALERTNAME}} / {{CLASSIFICATION}}\n" +
		"cluster {{CLUSTER_ID}} tenant {{TENANT_ID}}\n" +
		"proxy {{PROXY_URL}}\n" +
		"report to {{CALLBACK_URL}}\n{{NOT_A_FIELD}}\n"
	if err := os.WriteFile(promptPath, []byte(body), 0o600); err != nil {
		t.Fatal(err)
	}
	return New(Config{
		UsherURL:       usherURL,
		UsherPublicURL: "https://rap.example.com",
		PromptPath:     promptPath,
		Cwd:            "/home/dev/work",
		ProxyURL:       proxyURL,
	}, discardLogger())
}

func post(t *testing.T, h http.Handler, body string) *httptest.ResponseRecorder {
	t.Helper()
	req := httptest.NewRequest(http.MethodPost, "/dispatch", strings.NewReader(body))
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)
	return rec
}

func TestDispatchStartsSessionAndReportsRunning(t *testing.T) {
	usher := &stubUsher{}
	usherSrv := httptest.NewServer(usher.handler())
	defer usherSrv.Close()

	reports := make(chan string, 4)
	callback := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		body, _ := io.ReadAll(r.Body)
		reports <- string(body)
	}))
	defer callback.Close()

	a := newTestAdapter(t, usherSrv.URL, "http://proxy:8990/")
	rec := post(t, a.Handler(), strings.Replace(sampleDispatch, "%s", callback.URL, 1))

	if rec.Code != http.StatusAccepted {
		t.Fatalf("status = %d, want 202; body %s", rec.Code, rec.Body.String())
	}
	var accepted struct {
		SessionID  string `json:"session_id"`
		SessionRef string `json:"session_ref"`
	}
	if err := json.Unmarshal(rec.Body.Bytes(), &accepted); err != nil {
		t.Fatal(err)
	}
	if accepted.SessionID != "sess-abc123" {
		t.Errorf("session_id = %q", accepted.SessionID)
	}
	if want := "https://rap.example.com/#/s/sess-abc123"; accepted.SessionRef != want {
		t.Errorf("session_ref = %q, want %q", accepted.SessionRef, want)
	}

	usher.mu.Lock()
	defer usher.mu.Unlock()
	if len(usher.requests) != 1 {
		t.Fatalf("usher got %d requests, want 1", len(usher.requests))
	}
	req := usher.requests[0]
	// Unattended: a permission prompt with nobody to answer it hangs the run.
	if req.AutoApprove == nil || !*req.AutoApprove {
		t.Error("auto_approve must be true for an unattended session")
	}
	if req.Cwd != "/home/dev/work" {
		t.Errorf("cwd = %q", req.Cwd)
	}
	// The prompt is delivered with the session; there is no second /send.
	if !strings.Contains(req.InitialMessage, "alert ALT-2607-42 / UnhealthyPod / page") {
		t.Errorf("prompt missing rendered alert fields:\n%s", req.InitialMessage)
	}
	if !strings.Contains(req.InitialMessage, "report to "+callback.URL) {
		t.Error("prompt missing callback URL")
	}
	if !strings.Contains(req.InitialMessage, "proxy http://proxy:8990\n") {
		t.Errorf("proxy URL not normalised in prompt:\n%s", req.InitialMessage)
	}

	select {
	case body := <-reports:
		if !strings.Contains(body, `"status":"running"`) {
			t.Errorf("first report = %s, want status running", body)
		}
		if !strings.Contains(body, "sess-abc123") {
			t.Errorf("first report carries no session_ref: %s", body)
		}
	case <-time.After(3 * time.Second):
		t.Fatal("no running report reached the callback")
	}
}

// The status codes are alertstore's retry contract: 4xx stops the outbox, 5xx
// comes back. Getting these the wrong way round either loses alerts or hammers
// a broken endpoint eight times.
func TestDispatchStatusCodesMatchRetryTaxonomy(t *testing.T) {
	usher := &stubUsher{}
	usherSrv := httptest.NewServer(usher.handler())
	defer usherSrv.Close()

	cases := []struct {
		name string
		body string
		want int
	}{
		{"unparseable body", `not json`, http.StatusBadRequest},
		{"missing alert id", `{"callback":{"url":"http://x","token":"t"}}`, http.StatusBadRequest},
		{"missing callback", `{"alert_id":"ALT-1"}`, http.StatusBadRequest},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			a := newTestAdapter(t, usherSrv.URL, "http://proxy:8990")
			if rec := post(t, a.Handler(), tc.body); rec.Code != tc.want {
				t.Errorf("status = %d, want %d (%s)", rec.Code, tc.want, rec.Body.String())
			}
		})
	}
}

func TestDispatchReturns502WhenUsherIsUnreachable(t *testing.T) {
	a := newTestAdapter(t, "http://127.0.0.1:1", "http://proxy:8990")
	rec := post(t, a.Handler(), strings.Replace(sampleDispatch, "%s", "http://127.0.0.1:1", 1))
	if rec.Code != http.StatusBadGateway {
		t.Errorf("status = %d, want 502 so the outbox retries", rec.Code)
	}
}

func TestDispatchReturns502WhenUsherRefuses(t *testing.T) {
	usher := &stubUsher{status: http.StatusInternalServerError}
	usherSrv := httptest.NewServer(usher.handler())
	defer usherSrv.Close()

	a := newTestAdapter(t, usherSrv.URL, "http://proxy:8990")
	rec := post(t, a.Handler(), strings.Replace(sampleDispatch, "%s", "http://127.0.0.1:1", 1))
	if rec.Code != http.StatusBadGateway {
		t.Errorf("status = %d, want 502", rec.Code)
	}
}


// A placeholder the adapter does not know is left standing. Replacing it with
// an empty string would let a misspelling silently delete a section of the
// prompt, and the only symptom would be an agent ignoring instructions.
func TestRenderPromptLeavesUnknownPlaceholders(t *testing.T) {
	got := renderPrompt("{{ALERT_ID}} {{NOPE}}", &Dispatch{AlertID: "ALT-9"}, "http://proxy")
	if got != "ALT-9 {{NOPE}}" {
		t.Errorf("got %q", got)
	}
}

func TestBackfillFromOldDispatchFormat(t *testing.T) {
	d := &Dispatch{
		AlertID: "ALT-1",
		Alert:   json.RawMessage(`{"alertname":"TiKVDiskFull","classification":"page","cluster_id":"c-1","tenant_id":"t-1","attrs":{"description":"disk usage > 90%"}}`),
		Callback: Callback{URL: "http://x"},
	}
	d.backfillFromAlert()
	if d.Alertname != "TiKVDiskFull" {
		t.Errorf("alertname = %q", d.Alertname)
	}
	if d.Classification != "page" {
		t.Errorf("classification = %q", d.Classification)
	}
	if d.ClusterID != "c-1" {
		t.Errorf("cluster_id = %q", d.ClusterID)
	}
	if d.Description != "disk usage > 90%" {
		t.Errorf("description = %q", d.Description)
	}
}

func TestBackfillSkippedWhenNewFieldsPresent(t *testing.T) {
	d := &Dispatch{
		AlertID:   "ALT-1",
		Alertname: "NewName",
		Alert:     json.RawMessage(`{"alertname":"OldName"}`),
		Callback:  Callback{URL: "http://x"},
	}
	d.backfillFromAlert()
	if d.Alertname != "NewName" {
		t.Errorf("alertname = %q, want NewName (should not overwrite)", d.Alertname)
	}
}

func TestHealthz(t *testing.T) {
	a := newTestAdapter(t, "http://127.0.0.1:1", "http://proxy")
	rec := httptest.NewRecorder()
	a.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/healthz", nil))
	if rec.Code != http.StatusOK {
		t.Errorf("status = %d", rec.Code)
	}
}
