package apps

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

	"codeberg.org/VARASYS/ZDDC/zddc/internal/zddc"
)

func TestMatchAppHTML(t *testing.T) {
	cases := []struct {
		path, wantApp, wantDir string
	}{
		{"/", "landing", ""},
		{"/index.html", "landing", ""},
		{"/archive.html", "archive", ""},
		{"/Project-X/archive.html", "archive", "Project-X"},
		{"/Project-X/Working/browse.html", "browse", "Project-X/Working"},
		{"/foo.html", "", ""},
	}
	for _, tc := range cases {
		t.Run(tc.path, func(t *testing.T) {
			gotApp, gotDir := MatchAppHTML(tc.path)
			if gotApp != tc.wantApp || gotDir != tc.wantDir {
				t.Errorf("got (%q,%q), want (%q,%q)", gotApp, gotDir, tc.wantApp, tc.wantDir)
			}
		})
	}
}

// serve runs srv.Serve for app and returns the recorder.
func serve(srv *Server, app string) *httptest.ResponseRecorder {
	rec := httptest.NewRecorder()
	chain := zddc.PolicyChain{Levels: []zddc.ZddcFile{{}}}
	srv.Serve(rec, httptest.NewRequest(http.MethodGet, "/"+app+".html", nil), app, chain, srv.Root)
	return rec
}

func TestServer_NoBundle_ServesEmbedded(t *testing.T) {
	srv := NewServer(t.TempDir(), "test")
	saved := embeddedArchive
	embeddedArchive = []byte("EMBEDDED archive")
	defer func() { embeddedArchive = saved }()

	rec := serve(srv, "archive")
	if rec.Code != http.StatusOK {
		t.Fatalf("status=%d", rec.Code)
	}
	if !strings.Contains(rec.Body.String(), "EMBEDDED") {
		t.Errorf("expected embedded body, got %q", rec.Body.String())
	}
	if !strings.HasPrefix(rec.Header().Get("X-ZDDC-Source"), "embedded:archive@") {
		t.Errorf("X-ZDDC-Source=%q", rec.Header().Get("X-ZDDC-Source"))
	}
}

func TestServer_BundleMemberOverridesEmbedded(t *testing.T) {
	root := t.TempDir()
	writeTestBundle(t, root, map[string]string{"archive.html": "BUNDLE archive override"})
	srv := NewServer(root, "test")
	saved := embeddedArchive
	embeddedArchive = []byte("EMBEDDED archive")
	defer func() { embeddedArchive = saved }()

	rec := serve(srv, "archive")
	if rec.Code != http.StatusOK {
		t.Fatalf("status=%d", rec.Code)
	}
	if !strings.Contains(rec.Body.String(), "BUNDLE archive override") {
		t.Errorf("expected bundle body, got %q", rec.Body.String())
	}
	if rec.Header().Get("X-ZDDC-Source") != "bundle:archive.html" {
		t.Errorf("X-ZDDC-Source=%q, want bundle:archive.html", rec.Header().Get("X-ZDDC-Source"))
	}
}

func TestServer_BundlePresent_MemberAbsent_ServesEmbedded(t *testing.T) {
	root := t.TempDir()
	writeTestBundle(t, root, map[string]string{"browse.html": "BUNDLE browse"})
	srv := NewServer(root, "test")
	saved := embeddedArchive
	embeddedArchive = []byte("EMBEDDED archive")
	defer func() { embeddedArchive = saved }()

	rec := serve(srv, "archive") // bundle has browse, not archive
	if !strings.Contains(rec.Body.String(), "EMBEDDED") {
		t.Errorf("expected embedded fallback, got %q", rec.Body.String())
	}
}

func TestServer_UnknownTool_503WithoutBundle(t *testing.T) {
	srv := NewServer(t.TempDir(), "test")
	rec := serve(srv, "nope") // not embedded, no bundle
	if rec.Code != http.StatusServiceUnavailable {
		t.Errorf("status=%d, want 503", rec.Code)
	}
}

func TestServer_Embedded_ConditionalGET(t *testing.T) {
	srv := NewServer(t.TempDir(), "test")

	saved := embeddedArchive
	embeddedArchive = []byte("EMBEDDED archive bytes for ETag test")
	defer func() {
		embeddedArchive = saved
		etagCacheByApp.Delete("archive")
	}()
	etagCacheByApp.Delete("archive")

	rec1 := serve(srv, "archive")
	if rec1.Code != http.StatusOK {
		t.Fatalf("first GET: status=%d body=%s", rec1.Code, rec1.Body.String())
	}
	etag := rec1.Header().Get("ETag")
	if etag == "" {
		t.Fatalf("first GET: missing ETag header")
	}
	if cc := rec1.Header().Get("Cache-Control"); !strings.Contains(cc, "max-age=0") || !strings.Contains(cc, "must-revalidate") {
		t.Errorf("first GET: Cache-Control=%q", cc)
	}

	// Matching If-None-Match → 304, empty body.
	rec2 := httptest.NewRecorder()
	req2 := httptest.NewRequest(http.MethodGet, "/archive.html", nil)
	req2.Header.Set("If-None-Match", etag)
	chain := zddc.PolicyChain{Levels: []zddc.ZddcFile{{}}}
	srv.Serve(rec2, req2, "archive", chain, srv.Root)
	if rec2.Code != http.StatusNotModified {
		t.Fatalf("If-None-Match match: status=%d (want 304)", rec2.Code)
	}
	if rec2.Body.Len() != 0 {
		t.Errorf("304 response should have empty body; got %d bytes", rec2.Body.Len())
	}

	// Stale If-None-Match → 200, full body.
	rec3 := httptest.NewRecorder()
	req3 := httptest.NewRequest(http.MethodGet, "/archive.html", nil)
	req3.Header.Set("If-None-Match", `"deadbeef"`)
	srv.Serve(rec3, req3, "archive", chain, srv.Root)
	if rec3.Code != http.StatusOK || rec3.Body.Len() == 0 {
		t.Errorf("stale If-None-Match: status=%d bodyLen=%d (want 200, non-empty)", rec3.Code, rec3.Body.Len())
	}
}

// Bundle responses get a body-hash ETag and also short-circuit to 304.
func TestServer_Bundle_ConditionalGET(t *testing.T) {
	root := t.TempDir()
	writeTestBundle(t, root, map[string]string{"browse.html": "BUNDLE browse body"})
	srv := NewServer(root, "test")

	rec1 := serve(srv, "browse")
	etag := rec1.Header().Get("ETag")
	if rec1.Code != http.StatusOK || etag == "" {
		t.Fatalf("first GET: status=%d etag=%q", rec1.Code, etag)
	}
	rec2 := httptest.NewRecorder()
	req2 := httptest.NewRequest(http.MethodGet, "/browse.html", nil)
	req2.Header.Set("If-None-Match", etag)
	chain := zddc.PolicyChain{Levels: []zddc.ZddcFile{{}}}
	srv.Serve(rec2, req2, "browse", chain, srv.Root)
	if rec2.Code != http.StatusNotModified {
		t.Errorf("bundle If-None-Match: status=%d (want 304)", rec2.Code)
	}
}

// TestEmbeddedETag_Stable asserts EmbeddedETag is deterministic and
// content-addressed: same bytes → same ETag, different bytes → different.
func TestEmbeddedETag_Stable(t *testing.T) {
	saved := embeddedArchive
	defer func() {
		embeddedArchive = saved
		etagCacheByApp.Delete("archive")
	}()

	embeddedArchive = []byte("alpha")
	etagCacheByApp.Delete("archive")
	a1 := EmbeddedETag("archive")
	a2 := EmbeddedETag("archive")
	if a1 == "" || a1 != a2 {
		t.Errorf("EmbeddedETag should be stable for same bytes; got %q vs %q", a1, a2)
	}

	embeddedArchive = []byte("beta")
	etagCacheByApp.Delete("archive")
	b := EmbeddedETag("archive")
	if b == a1 {
		t.Errorf("EmbeddedETag should change with bytes; both %q", b)
	}
}
