package web

import (
	"bytes"
	"encoding/json"
	"io"
	"log"
	"net/http/httptest"
	"path/filepath"
	"strconv"
	"strings"
	"testing"
	"time"

	"gitea.dooplex.hu/admin/felhom-hub/internal/store"
)

func newRenderServer(t *testing.T) (*Server, *store.Store) {
	t.Helper()
	st, err := store.New(filepath.Join(t.TempDir(), "render.db"), log.New(io.Discard, "", 0))
	if err != nil {
		t.Fatalf("store.New: %v", err)
	}
	t.Cleanup(func() { st.Close() })
	return New(st, "", "", "test", time.Hour, log.New(io.Discard, "", 0)), st
}

// Part A + H UI — the expanded issue row renders the FULL message in a copyable <pre>,
// the context block with provenance, the explicit affected-customers list, the dismissal
// controls, and the customer-filter header.
func TestAppDetail_ExpandableIssueRender(t *testing.T) {
	s, _ := newRenderServer(t)

	fullMsg := "ERROR: nfs mount /mnt/media lost — stale file handle on /mnt/media/books (retry 5/5, giving up)"
	dismissed := time.Now()
	data := map[string]interface{}{
		"AppName": "cwa",
		"Issues": []store.AppIssue{
			{
				ID: 7, AppName: "cwa", Fingerprint: "error: nfs mount <hex> lost", Severity: "error",
				Message: fullMsg, FirstSeen: time.Now().Add(-48 * time.Hour), LastSeen: time.Now(),
				OccurrenceCount:   42,
				AffectedCustomers: []string{"cust-a", "cust-b"},
				Context:           []string{"line before", fullMsg, "line after"},
				ContextCustomer:   "cust-a",
			},
			{
				ID: 8, AppName: "cwa", Fingerprint: "warn: slow", Severity: "warn",
				Message: "WARN: slow disk", FirstSeen: time.Now(), LastSeen: time.Now(),
				OccurrenceCount: 1, AffectedCustomers: []string{"cust-a"},
				DismissedAt: &dismissed,
			},
		},
		"ChartData":      ChartData{},
		"Period":         "24h",
		"CustomerFilter": "cust-a",
		"ShowDismissed":  true,
		"CSRFToken":      "tok",
		"Flash":          "",
	}
	var buf bytes.Buffer
	if err := s.templates.ExecuteTemplate(&buf, "app_detail.html", data); err != nil {
		t.Fatalf("render app_detail.html: %v", err)
	}
	body := buf.String()
	for _, want := range []string{
		fullMsg,                             // full message, not tooltip-only
		`id="issue-msg-7"`,                  // copyable <pre> target
		`id="issue-ctx-7"`,                  // context block target
		"Copy",                              // copy buttons
		"Context around first occurrence",   // context label
		`/customers/cust-a`,                 // provenance link
		"line before",                       // context content
		"filtered: cust-a",                  // Part H header
		"Occurrences (all customers)",       // fleet-total label
		"Dismiss Selected",                  // Part G buttons
		"Dismiss All Issues",                //
		"/apps/cwa/dismiss-issues",          // dismissal route
		">dismissed<",                       // dismissed badge on row 8
		`</a>, <a href="/customers/cust-b"`, // explicit affected-customers list (linked, comma-separated)
	} {
		if !strings.Contains(body, want) {
			t.Errorf("app_detail.html missing %q", want)
		}
	}
	// the old hard-delete wording must be gone
	if strings.Contains(body, "Delete All Issues") || strings.Contains(body, "delete-issues") {
		t.Errorf("app_detail.html still renders hard-delete issue controls")
	}
}

// Part D UI — the customer page renders the per-app Request button (or the pending
// badge), the ?customer= drill-down link, and the received-tails section. Driven
// through the REAL handler + store (the sections live inside {{if .HasReports}}).
func TestCustomerUnified_LogTailSectionsRender(t *testing.T) {
	s, st := newRenderServer(t)

	if err := st.SaveReport("cust-a", []byte(`{"customer_id":"cust-a","customer_name":"Cust A","controller_version":"0.111.0"}`)); err != nil {
		t.Fatalf("SaveReport: %v", err)
	}
	// two telemetry apps: one gets a pending tail request, one has a received tail
	recA := mkWebTelemetryRecord(t, "gokapi", "Gokapi")
	recB := mkWebTelemetryRecord(t, "cwa", "Calibre-Web")
	if err := st.SaveAppTelemetry("cust-a", time.Now(), []store.AppTelemetryRecord{recA, recB}); err != nil {
		t.Fatalf("SaveAppTelemetry: %v", err)
	}
	if err := st.RequestLogTail("cust-a", "cwa"); err != nil {
		t.Fatalf("RequestLogTail: %v", err)
	}
	if err := st.SaveAppLogTail("cust-a", "gokapi", time.Now(), []string{"a", "b"}); err != nil {
		t.Fatalf("SaveAppLogTail: %v", err)
	}
	tails, _ := st.GetCustomerLogTails("cust-a")
	if len(tails) != 1 {
		t.Fatalf("expected 1 stored tail")
	}
	tailID := strconv.Itoa(tails[0].ID)

	rr := httptest.NewRecorder()
	s.handleCustomerUnified(rr, httptest.NewRequest("GET", "/customers/cust-a", nil), "cust-a")
	body := rr.Body.String()
	for _, want := range []string{
		"/apps/gokapi?customer=cust-a",       // Part H drill-down carries the filter
		"/customers/cust-a/request-log-tail", // request form
		"Request log tail",                   // the button (gokapi row)
		"tail pending",                       // the badge (cwa row, pending request)
		"App Log Tails",                      // received-tails section
		"/customers/cust-a/log-tail/" + tailID,
		"/customers/cust-a/log-tail/" + tailID + "?download=1",
	} {
		if !strings.Contains(body, want) {
			t.Errorf("customer page missing %q", want)
		}
	}
}

// mkWebTelemetryRecord builds a minimal AppTelemetryRecord via JSON (the wire path).
func mkWebTelemetryRecord(t *testing.T, app, display string) store.AppTelemetryRecord {
	t.Helper()
	var rec store.AppTelemetryRecord
	if err := json.Unmarshal([]byte(`{"app_name":"`+app+`","display_name":"`+display+`"}`), &rec); err != nil {
		t.Fatalf("record: %v", err)
	}
	return rec
}

// The tail view renders ordered lines; ?download=1 serves a plain-text .log attachment.
func TestLogTailView_RenderAndDownload(t *testing.T) {
	s, st := newRenderServer(t)
	if err := st.SaveAppLogTail("cust-a", "gokapi", time.Now(), []string{"first line", "second line", "third line"}); err != nil {
		t.Fatalf("SaveAppLogTail: %v", err)
	}
	tails, _ := st.GetCustomerLogTails("cust-a")
	if len(tails) != 1 {
		t.Fatalf("expected 1 stored tail")
	}
	id := strconv.Itoa(tails[0].ID)

	// HTML view — ordered lines present
	rr := httptest.NewRecorder()
	s.handleLogTailView(rr, httptest.NewRequest("GET", "/customers/cust-a/log-tail/"+id, nil), "cust-a", id)
	body := rr.Body.String()
	iFirst := strings.Index(body, "first line")
	iSecond := strings.Index(body, "second line")
	iThird := strings.Index(body, "third line")
	if iFirst < 0 || iSecond < iFirst || iThird < iSecond {
		t.Fatalf("tail view lines missing or out of order: %d/%d/%d", iFirst, iSecond, iThird)
	}

	// download — text/plain attachment with the raw lines
	rr = httptest.NewRecorder()
	s.handleLogTailView(rr, httptest.NewRequest("GET", "/customers/cust-a/log-tail/"+id+"?download=1", nil), "cust-a", id)
	if ct := rr.Header().Get("Content-Type"); !strings.HasPrefix(ct, "text/plain") {
		t.Fatalf("download content-type = %q", ct)
	}
	if cd := rr.Header().Get("Content-Disposition"); !strings.Contains(cd, "attachment") || !strings.Contains(cd, ".log") {
		t.Fatalf("download disposition = %q", cd)
	}
	if got := rr.Body.String(); got != "first line\nsecond line\nthird line\n" {
		t.Fatalf("download body = %q", got)
	}

	// cross-customer read → 404
	rr = httptest.NewRecorder()
	s.handleLogTailView(rr, httptest.NewRequest("GET", "/customers/cust-b/log-tail/"+id, nil), "cust-b", id)
	if rr.Code != 404 {
		t.Fatalf("cross-customer tail view = %d, want 404", rr.Code)
	}
}
