package httpapi_test

import (
	"bytes"
	"context"
	"crypto/ed25519"
	"crypto/rand"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"momswap/backend/internal/app"
	httpapi "momswap/backend/internal/http"
	"momswap/backend/internal/store"
)

func newTestServer(adminPublicKey string) *httptest.Server {
	memory := store.NewMemoryStore()
	svc := app.NewService(memory, app.Config{
		ChallengeTTL: 5 * time.Minute,
		SessionTTL:   24 * time.Hour,
	}, adminPublicKey)
	svc.BootstrapAdmin(adminPublicKey)
	svc.ConfigureAssetStorage(fakeSigner{})
	api := httpapi.NewAPI(svc)
	return httptest.NewServer(api.Routes())
}

type fakeSigner struct{}

func (fakeSigner) SignedGetObjectURL(_ context.Context, objectKey string, _ time.Duration) (string, error) {
	return "http://files.local/download/" + objectKey, nil
}

func (fakeSigner) PutObject(_ context.Context, _ string, _ string, _ io.Reader, _ int64) error {
	return nil
}

func (fakeSigner) GetObject(_ context.Context, objectKey string) (io.ReadCloser, string, int64, error) {
	payload := []byte("fake-download:" + objectKey)
	return io.NopCloser(bytes.NewReader(payload)), "application/octet-stream", int64(len(payload)), nil
}

func mustJSON(t *testing.T, value interface{}) []byte {
	t.Helper()
	b, err := json.Marshal(value)
	if err != nil {
		t.Fatalf("marshal json: %v", err)
	}
	return b
}

func getJSON(t *testing.T, client *http.Client, url string, token string) (*http.Response, map[string]interface{}) {
	t.Helper()
	req, _ := http.NewRequest(http.MethodGet, url, nil)
	if token != "" {
		req.Header.Set("Authorization", "Bearer "+token)
	}
	resp, err := client.Do(req)
	if err != nil {
		t.Fatalf("do request: %v", err)
	}
	defer resp.Body.Close()
	out := map[string]interface{}{}
	_ = json.NewDecoder(resp.Body).Decode(&out)
	return resp, out
}

func postJSON(t *testing.T, client *http.Client, url string, body interface{}, token string) (*http.Response, map[string]interface{}) {
	t.Helper()
	req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(mustJSON(t, body)))
	if err != nil {
		t.Fatalf("new request: %v", err)
	}
	req.Header.Set("Content-Type", "application/json")
	if token != "" {
		req.Header.Set("Authorization", "Bearer "+token)
	}
	resp, err := client.Do(req)
	if err != nil {
		t.Fatalf("do request: %v", err)
	}
	defer resp.Body.Close()
	out := map[string]interface{}{}
	_ = json.NewDecoder(resp.Body).Decode(&out)
	return resp, out
}

func patchJSON(t *testing.T, client *http.Client, url string, body interface{}, token string) (*http.Response, map[string]interface{}) {
	t.Helper()
	req, err := http.NewRequest(http.MethodPatch, url, bytes.NewReader(mustJSON(t, body)))
	if err != nil {
		t.Fatalf("new request: %v", err)
	}
	req.Header.Set("Content-Type", "application/json")
	if token != "" {
		req.Header.Set("Authorization", "Bearer "+token)
	}
	resp, err := client.Do(req)
	if err != nil {
		t.Fatalf("do request: %v", err)
	}
	defer resp.Body.Close()
	out := map[string]interface{}{}
	_ = json.NewDecoder(resp.Body).Decode(&out)
	return resp, out
}

func putRaw(t *testing.T, client *http.Client, url string, payload []byte, contentType string, token string) *http.Response {
	t.Helper()
	req, err := http.NewRequest(http.MethodPut, url, bytes.NewReader(payload))
	if err != nil {
		t.Fatalf("new request: %v", err)
	}
	if contentType != "" {
		req.Header.Set("Content-Type", contentType)
	}
	if token != "" {
		req.Header.Set("Authorization", "Bearer "+token)
	}
	resp, err := client.Do(req)
	if err != nil {
		t.Fatalf("do request: %v", err)
	}
	return resp
}

func loginUser(t *testing.T, client *http.Client, baseURL, pubB64 string, priv ed25519.PrivateKey) string {
	t.Helper()
	chResp, chData := postJSON(t, client, baseURL+"/v1/auth/challenge", map[string]string{"publicKey": pubB64}, "")
	if chResp.StatusCode != http.StatusOK {
		t.Fatalf("create challenge status=%d body=%v", chResp.StatusCode, chData)
	}
	nonce := chData["nonce"].(string)
	sig := ed25519.Sign(priv, []byte("login:"+nonce))
	loginResp, loginData := postJSON(t, client, baseURL+"/v1/auth/login", map[string]string{
		"publicKey": pubB64,
		"nonce":     nonce,
		"signature": base64.RawURLEncoding.EncodeToString(sig),
	}, "")
	if loginResp.StatusCode != http.StatusOK {
		t.Fatalf("login status=%d body=%v", loginResp.StatusCode, loginData)
	}
	return loginData["accessToken"].(string)
}

func registerUserViaAdmin(t *testing.T, client *http.Client, baseURL, adminPub string, adminPriv ed25519.PrivateKey, adminToken string, userPub string, userPriv ed25519.PrivateKey, jti string) {
	t.Helper()
	payload := app.InvitationPayload{
		JTI:              jti,
		InviterPublicKey: adminPub,
		ExpiresAtUnix:    time.Now().Add(time.Hour).Unix(),
		MaxUses:          1,
	}
	payloadRaw := mustJSON(t, payload)
	payloadB64 := base64.RawURLEncoding.EncodeToString(payloadRaw)
	inviteSig := base64.RawURLEncoding.EncodeToString(ed25519.Sign(adminPriv, []byte("invite:"+payloadB64)))

	inviteResp, inviteData := postJSON(t, client, baseURL+"/v1/invitations", map[string]string{
		"invitePayloadB64": payloadB64,
		"inviteSignature":  inviteSig,
	}, adminToken)
	if inviteResp.StatusCode != http.StatusCreated {
		t.Fatalf("create invitation status=%d body=%v", inviteResp.StatusCode, inviteData)
	}

	proofSig := base64.RawURLEncoding.EncodeToString(ed25519.Sign(userPriv, []byte("register:"+userPub+":"+jti)))
	registerResp, registerData := postJSON(t, client, baseURL+"/v1/auth/register", map[string]string{
		"publicKey":        userPub,
		"invitePayloadB64": payloadB64,
		"inviteSignature":  inviteSig,
		"proofSignature":   proofSig,
	}, "")
	if registerResp.StatusCode != http.StatusCreated {
		t.Fatalf("register status=%d body=%v", registerResp.StatusCode, registerData)
	}
}

func TestRegisterBySignature(t *testing.T) {
	adminPub, _, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		t.Fatalf("generate admin key: %v", err)
	}
	adminPubB64 := base64.RawURLEncoding.EncodeToString(adminPub)

	server := newTestServer(adminPubB64)
	defer server.Close()
	client := server.Client()

	svcKeyResp, svcKeyData := getJSON(t, client, server.URL+"/v1/service-key", "")
	if svcKeyResp.StatusCode != http.StatusOK {
		t.Fatalf("get service key status=%d body=%v", svcKeyResp.StatusCode, svcKeyData)
	}
	if svcKeyData["publicKey"] != adminPubB64 {
		t.Fatalf("service key mismatch: got %v", svcKeyData["publicKey"])
	}

	userPub, userPriv, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		t.Fatalf("generate user key: %v", err)
	}
	userPubB64 := base64.RawURLEncoding.EncodeToString(userPub)
	sig := base64.RawURLEncoding.EncodeToString(ed25519.Sign(userPriv, []byte(adminPubB64)))

	regResp, regData := postJSON(t, client, server.URL+"/v1/auth/register-by-signature", map[string]string{
		"publicKey": userPubB64,
		"signature": sig,
	}, "")
	if regResp.StatusCode != http.StatusCreated {
		t.Fatalf("register-by-signature status=%d body=%v", regResp.StatusCode, regData)
	}

	userToken := loginUser(t, client, server.URL, userPubB64, userPriv)
	if userToken == "" {
		t.Fatal("login after register-by-signature failed")
	}
}

func TestRegisterLoginAndProfile(t *testing.T) {
	adminPub, adminPriv, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		t.Fatalf("generate admin key: %v", err)
	}
	adminPubB64 := base64.RawURLEncoding.EncodeToString(adminPub)

	server := newTestServer(adminPubB64)
	defer server.Close()
	client := server.Client()

	adminToken := loginUser(t, client, server.URL, adminPubB64, adminPriv)

	userPub, userPriv, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		t.Fatalf("generate user key: %v", err)
	}
	userPubB64 := base64.RawURLEncoding.EncodeToString(userPub)
	registerUserViaAdmin(t, client, server.URL, adminPubB64, adminPriv, adminToken, userPubB64, userPriv, "invite-1")

	userToken := loginUser(t, client, server.URL, userPubB64, userPriv)
	req, _ := http.NewRequest(http.MethodGet, server.URL+"/v1/me/keys", nil)
	req.Header.Set("Authorization", "Bearer "+userToken)
	resp, err := client.Do(req)
	if err != nil {
		t.Fatalf("me request: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("me status=%d", resp.StatusCode)
	}
}

func TestCollectionOwnershipIsolation(t *testing.T) {
	adminPub, adminPriv, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		t.Fatalf("generate admin key: %v", err)
	}
	adminPubB64 := base64.RawURLEncoding.EncodeToString(adminPub)

	server := newTestServer(adminPubB64)
	defer server.Close()
	client := server.Client()

	adminToken := loginUser(t, client, server.URL, adminPubB64, adminPriv)

	user1Pub, user1Priv, _ := ed25519.GenerateKey(rand.Reader)
	user1PubB64 := base64.RawURLEncoding.EncodeToString(user1Pub)
	registerUserViaAdmin(t, client, server.URL, adminPubB64, adminPriv, adminToken, user1PubB64, user1Priv, "invite-u1")
	user1Token := loginUser(t, client, server.URL, user1PubB64, user1Priv)

	user2Pub, user2Priv, _ := ed25519.GenerateKey(rand.Reader)
	user2PubB64 := base64.RawURLEncoding.EncodeToString(user2Pub)
	registerUserViaAdmin(t, client, server.URL, adminPubB64, adminPriv, adminToken, user2PubB64, user2Priv, "invite-u2")
	user2Token := loginUser(t, client, server.URL, user2PubB64, user2Priv)

	createCollectionResp, createCollectionData := postJSON(t, client, server.URL+"/v1/collections", map[string]string{
		"name": "my places",
	}, user1Token)
	if createCollectionResp.StatusCode != http.StatusCreated {
		t.Fatalf("create collection status=%d body=%v", createCollectionResp.StatusCode, createCollectionData)
	}
	collectionID := createCollectionData["id"].(string)

	createFeatureResp, createFeatureData := postJSON(t, client, server.URL+"/v1/collections/"+collectionID+"/features", map[string]interface{}{
		"geometry": map[string]interface{}{
			"type":        "Point",
			"coordinates": []float64{-16.6291, 28.4636},
		},
		"properties": map[string]interface{}{
			"name": "Santa Cruz",
		},
	}, user1Token)
	if createFeatureResp.StatusCode != http.StatusCreated {
		t.Fatalf("create feature status=%d body=%v", createFeatureResp.StatusCode, createFeatureData)
	}

	req, _ := http.NewRequest(http.MethodGet, server.URL+"/v1/collections/"+collectionID+"/features", nil)
	req.Header.Set("Authorization", "Bearer "+user2Token)
	resp, err := client.Do(req)
	if err != nil {
		t.Fatalf("list features as user2: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusForbidden {
		t.Fatalf("expected 403, got %d", resp.StatusCode)
	}

	featureID := createFeatureData["id"].(string)

	patchOwnResp, patchOwnData := patchJSON(t, client, server.URL+"/v1/features/"+featureID, map[string]interface{}{
		"geometry": map[string]interface{}{
			"type":        "Point",
			"coordinates": []float64{-16.6299, 28.4639, 11},
		},
	}, user1Token)
	if patchOwnResp.StatusCode != http.StatusOK {
		t.Fatalf("owner patch feature status=%d body=%v", patchOwnResp.StatusCode, patchOwnData)
	}

	patchOtherResp, patchOtherData := patchJSON(t, client, server.URL+"/v1/features/"+featureID, map[string]interface{}{
		"geometry": map[string]interface{}{
			"type":        "Point",
			"coordinates": []float64{-16.6301, 28.4641, 12},
		},
	}, user2Token)
	if patchOtherResp.StatusCode != http.StatusForbidden {
		t.Fatalf("non-owner patch feature status=%d body=%v", patchOtherResp.StatusCode, patchOtherData)
	}
}

func TestAssetLifecycleAndVisibility(t *testing.T) {
	adminPub, adminPriv, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		t.Fatalf("generate admin key: %v", err)
	}
	adminPubB64 := base64.RawURLEncoding.EncodeToString(adminPub)
	server := newTestServer(adminPubB64)
	defer server.Close()
	client := server.Client()

	adminToken := loginUser(t, client, server.URL, adminPubB64, adminPriv)

	user1Pub, user1Priv, _ := ed25519.GenerateKey(rand.Reader)
	user1PubB64 := base64.RawURLEncoding.EncodeToString(user1Pub)
	registerUserViaAdmin(t, client, server.URL, adminPubB64, adminPriv, adminToken, user1PubB64, user1Priv, "invite-asset-u1")
	user1Token := loginUser(t, client, server.URL, user1PubB64, user1Priv)

	user2Pub, user2Priv, _ := ed25519.GenerateKey(rand.Reader)
	user2PubB64 := base64.RawURLEncoding.EncodeToString(user2Pub)
	registerUserViaAdmin(t, client, server.URL, adminPubB64, adminPriv, adminToken, user2PubB64, user2Priv, "invite-asset-u2")

	createCollectionResp, createCollectionData := postJSON(t, client, server.URL+"/v1/collections", map[string]string{
		"name": "assets",
	}, user1Token)
	if createCollectionResp.StatusCode != http.StatusCreated {
		t.Fatalf("create collection status=%d body=%v", createCollectionResp.StatusCode, createCollectionData)
	}
	collectionID := createCollectionData["id"].(string)

	createFeatureResp, createFeatureData := postJSON(t, client, server.URL+"/v1/collections/"+collectionID+"/features", map[string]interface{}{
		"geometry": map[string]interface{}{
			"type":        "Point",
			"coordinates": []float64{-16.6291, 28.4636, 22},
		},
		"properties": map[string]interface{}{
			"name": "feature-a",
		},
	}, user1Token)
	if createFeatureResp.StatusCode != http.StatusCreated {
		t.Fatalf("create feature status=%d body=%v", createFeatureResp.StatusCode, createFeatureData)
	}
	featureID := createFeatureData["id"].(string)

	createAssetResp, createAssetData := postJSON(t, client, server.URL+"/v1/assets", map[string]interface{}{
		"featureId":   featureID,
		"checksum":    "ABCDEF1234",
		"ext":         "glb",
		"kind":        "3d",
		"mimeType":    "model/gltf-binary",
		"sizeBytes":   100,
		"name":        "Tree",
		"description": "Public tree",
		"isPublic":    true,
	}, user1Token)
	if createAssetResp.StatusCode != http.StatusCreated {
		t.Fatalf("create asset status=%d body=%v", createAssetResp.StatusCode, createAssetData)
	}
	asset := createAssetData["asset"].(map[string]interface{})
	assetID := asset["id"].(string)

	createAssetResp2, createAssetData2 := postJSON(t, client, server.URL+"/v1/assets", map[string]interface{}{
		"featureId": featureID,
		"checksum":  "abcdef1234",
		"ext":       "glb",
		"kind":      "3d",
		"name":      "Tree v2",
	}, user1Token)
	if createAssetResp2.StatusCode != http.StatusOK {
		t.Fatalf("dedup create asset status=%d body=%v", createAssetResp2.StatusCode, createAssetData2)
	}
	asset2 := createAssetData2["asset"].(map[string]interface{})
	if asset2["id"].(string) != assetID {
		t.Fatalf("expected dedup asset id=%s got=%s", assetID, asset2["id"].(string))
	}

	uploadResp, uploadData := postJSON(t, client, server.URL+"/v1/assets/"+assetID+"/signed-upload", map[string]interface{}{
		"contentType": "model/gltf-binary",
	}, user1Token)
	if uploadResp.StatusCode != http.StatusOK {
		t.Fatalf("signed upload status=%d body=%v", uploadResp.StatusCode, uploadData)
	}
	if uploadData["url"] != "/v1/assets/"+assetID+"/upload" {
		t.Fatalf("unexpected signed-upload backend url: %v", uploadData["url"])
	}

	putResp := putRaw(t, client, server.URL+"/v1/assets/"+assetID+"/upload", []byte("glb-bytes"), "model/gltf-binary", user1Token)
	defer putResp.Body.Close()
	if putResp.StatusCode != http.StatusNoContent {
		t.Fatalf("upload proxy status=%d", putResp.StatusCode)
	}

	featuresResp, featuresData := getJSON(t, client, server.URL+"/v1/collections/"+collectionID+"/features", user1Token)
	if featuresResp.StatusCode != http.StatusOK {
		t.Fatalf("list features status=%d body=%v", featuresResp.StatusCode, featuresData)
	}
	features := featuresData["features"].([]interface{})
	firstFeature := features[0].(map[string]interface{})
	properties := firstFeature["properties"].(map[string]interface{})
	assets := properties["assets"].([]interface{})
	if len(assets) != 1 {
		t.Fatalf("expected 1 linked asset, got %d", len(assets))
	}
	assetView := assets[0].(map[string]interface{})
	if assetView["link"] != "/v1/assets/"+assetID+"/download" {
		t.Fatalf("unexpected asset link: %v", assetView["link"])
	}

	reqDownloadPublic, _ := http.NewRequest(http.MethodGet, server.URL+"/v1/assets/"+assetID+"/download", nil)
	downloadPublicResp, err := client.Do(reqDownloadPublic)
	if err != nil {
		t.Fatalf("download public request failed: %v", err)
	}
	defer downloadPublicResp.Body.Close()
	if downloadPublicResp.StatusCode != http.StatusOK {
		t.Fatalf("expected public asset stream status, got %d", downloadPublicResp.StatusCode)
	}
	body, err := io.ReadAll(downloadPublicResp.Body)
	if err != nil {
		t.Fatalf("read public download body: %v", err)
	}
	expectedBody := fmt.Sprintf("fake-download:%s/%s.%s", user1PubB64, "abcdef1234", "glb")
	if string(body) != expectedBody {
		t.Fatalf("unexpected download body: %q", string(body))
	}

	patchResp, patchData := patchJSON(t, client, server.URL+"/v1/assets/"+assetID, map[string]interface{}{
		"isPublic": false,
	}, user1Token)
	if patchResp.StatusCode != http.StatusOK {
		t.Fatalf("patch asset status=%d body=%v", patchResp.StatusCode, patchData)
	}

	reqDownloadPrivate, _ := http.NewRequest(http.MethodGet, server.URL+"/v1/assets/"+assetID+"/download", nil)
	downloadPrivateResp, err := client.Do(reqDownloadPrivate)
	if err != nil {
		t.Fatalf("download private request failed: %v", err)
	}
	if downloadPrivateResp.StatusCode != http.StatusForbidden {
		t.Fatalf("expected 403 for private asset, got %d", downloadPrivateResp.StatusCode)
	}
}

func TestListPublicFeatures(t *testing.T) {
	adminPub, adminPriv, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		t.Fatalf("generate admin key: %v", err)
	}
	adminPubB64 := base64.RawURLEncoding.EncodeToString(adminPub)
	server := newTestServer(adminPubB64)
	defer server.Close()
	client := server.Client()

	adminToken := loginUser(t, client, server.URL, adminPubB64, adminPriv)

	user1Pub, user1Priv, _ := ed25519.GenerateKey(rand.Reader)
	user1PubB64 := base64.RawURLEncoding.EncodeToString(user1Pub)
	registerUserViaAdmin(t, client, server.URL, adminPubB64, adminPriv, adminToken, user1PubB64, user1Priv, "invite-public-u1")
	user1Token := loginUser(t, client, server.URL, user1PubB64, user1Priv)

	user2Pub, user2Priv, _ := ed25519.GenerateKey(rand.Reader)
	user2PubB64 := base64.RawURLEncoding.EncodeToString(user2Pub)
	registerUserViaAdmin(t, client, server.URL, adminPubB64, adminPriv, adminToken, user2PubB64, user2Priv, "invite-public-u2")
	user2Token := loginUser(t, client, server.URL, user2PubB64, user2Priv)

	c1Resp, c1Data := postJSON(t, client, server.URL+"/v1/collections", map[string]string{"name": "u1-public"}, user1Token)
	if c1Resp.StatusCode != http.StatusCreated {
		t.Fatalf("u1 create collection status=%d body=%v", c1Resp.StatusCode, c1Data)
	}
	c1ID := c1Data["id"].(string)
	f1Resp, f1Data := postJSON(t, client, server.URL+"/v1/collections/"+c1ID+"/features", map[string]interface{}{
		"geometry": map[string]interface{}{"type": "Point", "coordinates": []float64{-16.25, 28.46, 5}},
		"properties": map[string]interface{}{
			"name": "u1-public-feature",
		},
	}, user1Token)
	if f1Resp.StatusCode != http.StatusCreated {
		t.Fatalf("u1 create feature status=%d body=%v", f1Resp.StatusCode, f1Data)
	}
	f1ID := f1Data["id"].(string)
	a1Resp, a1Data := postJSON(t, client, server.URL+"/v1/assets", map[string]interface{}{
		"featureId": f1ID,
		"checksum":  "pub3d111",
		"ext":       "glb",
		"kind":      "3d",
		"isPublic":  true,
	}, user1Token)
	if a1Resp.StatusCode != http.StatusCreated {
		t.Fatalf("u1 create public asset status=%d body=%v", a1Resp.StatusCode, a1Data)
	}
	a1ID := a1Data["asset"].(map[string]interface{})["id"].(string)

	c2Resp, c2Data := postJSON(t, client, server.URL+"/v1/collections", map[string]string{"name": "u2-private"}, user2Token)
	if c2Resp.StatusCode != http.StatusCreated {
		t.Fatalf("u2 create collection status=%d body=%v", c2Resp.StatusCode, c2Data)
	}
	c2ID := c2Data["id"].(string)
	f2Resp, f2Data := postJSON(t, client, server.URL+"/v1/collections/"+c2ID+"/features", map[string]interface{}{
		"geometry": map[string]interface{}{"type": "Point", "coordinates": []float64{-16.3, 28.47, 7}},
		"properties": map[string]interface{}{
			"name": "u2-private-feature",
		},
	}, user2Token)
	if f2Resp.StatusCode != http.StatusCreated {
		t.Fatalf("u2 create feature status=%d body=%v", f2Resp.StatusCode, f2Data)
	}
	f2ID := f2Data["id"].(string)
	a2Resp, a2Data := postJSON(t, client, server.URL+"/v1/assets", map[string]interface{}{
		"featureId": f2ID,
		"checksum":  "priv3d222",
		"ext":       "glb",
		"kind":      "3d",
		"isPublic":  false,
	}, user2Token)
	if a2Resp.StatusCode != http.StatusCreated {
		t.Fatalf("u2 create private asset status=%d body=%v", a2Resp.StatusCode, a2Data)
	}

	publicResp, publicData := getJSON(t, client, server.URL+"/v1/features/public?kind=3d", "")
	if publicResp.StatusCode != http.StatusOK {
		t.Fatalf("list public features status=%d body=%v", publicResp.StatusCode, publicData)
	}
	publicFeatures := publicData["features"].([]interface{})
	if len(publicFeatures) != 1 {
		t.Fatalf("expected 1 public feature, got %d", len(publicFeatures))
	}
	publicFeature := publicFeatures[0].(map[string]interface{})
	if publicFeature["id"].(string) != f1ID {
		t.Fatalf("expected public feature id=%s got=%v", f1ID, publicFeature["id"])
	}
	properties := publicFeature["properties"].(map[string]interface{})
	assets := properties["assets"].([]interface{})
	if len(assets) != 1 {
		t.Fatalf("expected 1 public asset, got %d", len(assets))
	}
	publicAsset := assets[0].(map[string]interface{})
	if publicAsset["id"].(string) != a1ID {
		t.Fatalf("expected public asset id=%s got=%v", a1ID, publicAsset["id"])
	}
}
