package main

import (
	"bytes"
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"path/filepath"
	"strings"
	"testing"

	"go.uber.org/zap"

	"iop/apps/control-plane/internal/credentialstore"
)

func TestCredentialHTTPRequiresPrincipalAndBoundsSecretBody(t *testing.T) {
	cfg := defaultConfig()
	cfg.Database.URL = filepath.Join(t.TempDir(), "credential-http.db")
	cfg.CredentialEncryption.KeyFile = writeKeyManifest(t, 0x51)
	cfg.CredentialEncryption.ActiveKeyID = "primary"
	cfg.CredentialEncryption.ActiveKeyVersion = 1
	runtime, err := composeCredentialRuntime(context.Background(), cfg, zap.NewNop())
	if err != nil {
		t.Fatal(err)
	}
	defer runtime.store.Close()
	first, err := runtime.store.CreatePrincipalWithToken(context.Background(), credentialstore.CreatePrincipalInput{Alias: "first"})
	if err != nil {
		t.Fatal(err)
	}
	second, err := runtime.store.CreatePrincipalWithToken(context.Background(), credentialstore.CreatePrincipalInput{Alias: "second"})
	if err != nil {
		t.Fatal(err)
	}
	mutations := 0
	mux := http.NewServeMux()
	registerCredentialHandlers(mux, runtime.service, func(*http.Request) error { mutations++; return nil })

	unauthorized := httptest.NewRecorder()
	mux.ServeHTTP(unauthorized, httptest.NewRequest(http.MethodGet, "/v1/credentials/slots", nil))
	if unauthorized.Code != http.StatusUnauthorized {
		t.Fatalf("unauthorized status = %d", unauthorized.Code)
	}

	wrongContent := httptest.NewRequest(http.MethodPost, "/v1/credentials/slots", strings.NewReader("secret"))
	wrongContent.Header.Set("Authorization", "Bearer "+first.RawToken)
	wrongResponse := httptest.NewRecorder()
	mux.ServeHTTP(wrongResponse, wrongContent)
	if wrongResponse.Code != http.StatusUnsupportedMediaType {
		t.Fatalf("wrong content status = %d", wrongResponse.Code)
	}

	oversize := httptest.NewRequest(http.MethodPost, "/v1/credentials/slots", bytes.NewReader(make([]byte, maxProviderSecretBytes+1)))
	oversize.Header.Set("Authorization", "Bearer "+first.RawToken)
	oversize.Header.Set("Content-Type", "application/octet-stream")
	oversizeResponse := httptest.NewRecorder()
	mux.ServeHTTP(oversizeResponse, oversize)
	if oversizeResponse.Code != http.StatusRequestEntityTooLarge || mutations != 0 {
		t.Fatalf("oversize status=%d mutations=%d", oversizeResponse.Code, mutations)
	}

	create := httptest.NewRequest(http.MethodPost, "/v1/credentials/slots", bytes.NewReader(bytes.Repeat([]byte{'s'}, maxProviderSecretBytes)))
	create.Header.Set("Authorization", "Bearer "+first.RawToken)
	create.Header.Set("Content-Type", "application/octet-stream")
	create.Header.Set("IOP-Credential-Vendor", "openai")
	create.Header.Set("IOP-Credential-Kind", "bearer")
	createdResponse := httptest.NewRecorder()
	mux.ServeHTTP(createdResponse, create)
	if createdResponse.Code != http.StatusOK || mutations != 1 {
		t.Fatalf("create status=%d mutations=%d body=%s", createdResponse.Code, mutations, createdResponse.Body.String())
	}
	var created struct {
		ID string `json:"ID"`
	}
	if err := json.Unmarshal(createdResponse.Body.Bytes(), &created); err != nil || created.ID == "" {
		t.Fatalf("decode created slot: id=%q err=%v", created.ID, err)
	}

	routeBody := `{"slot_id":"` + created.ID + `","alias":"ornith:35b","profile_id":"openai","upstream_model":"ornith:35b","resource_selector":"provider-slow"}`
	createRoute := httptest.NewRequest(http.MethodPost, "/v1/credentials/routes", strings.NewReader(routeBody))
	createRoute.Header.Set("Authorization", "Bearer "+first.RawToken)
	createRoute.Header.Set("Content-Type", "application/json")
	createRouteResponse := httptest.NewRecorder()
	mux.ServeHTTP(createRouteResponse, createRoute)
	var route struct {
		ID               string `json:"ID"`
		ResourceSelector string `json:"ResourceSelector"`
		Revision         int64  `json:"Revision"`
	}
	if err := json.Unmarshal(createRouteResponse.Body.Bytes(), &route); err != nil || createRouteResponse.Code != http.StatusOK || route.ID == "" || route.Revision != 0 {
		t.Fatalf("create route: status=%d route=%+v err=%v body=%s", createRouteResponse.Code, route, err, createRouteResponse.Body.String())
	}

	updateBody := `{"slot_id":"` + created.ID + `","alias":"ornith:35b","profile_id":"openai","upstream_model":"ornith:35b","resource_selector":"provider-fast"}`
	updateRoute := httptest.NewRequest(http.MethodPost, "/v1/credentials/routes/"+route.ID+"/update", strings.NewReader(updateBody))
	updateRoute.Header.Set("Authorization", "Bearer "+first.RawToken)
	updateRoute.Header.Set("Content-Type", "application/json")
	updateRoute.Header.Set("IOP-Expected-Revision", "0")
	updateRouteResponse := httptest.NewRecorder()
	mux.ServeHTTP(updateRouteResponse, updateRoute)
	if err := json.Unmarshal(updateRouteResponse.Body.Bytes(), &route); err != nil || updateRouteResponse.Code != http.StatusOK || route.ResourceSelector != "provider-fast" || route.Revision != 1 || mutations != 3 {
		t.Fatalf("update route: status=%d route=%+v mutations=%d err=%v body=%s", updateRouteResponse.Code, route, mutations, err, updateRouteResponse.Body.String())
	}

	staleRoute := httptest.NewRequest(http.MethodPost, "/v1/credentials/routes/"+route.ID+"/update", strings.NewReader(updateBody))
	staleRoute.Header.Set("Authorization", "Bearer "+first.RawToken)
	staleRoute.Header.Set("Content-Type", "application/json")
	staleRoute.Header.Set("IOP-Expected-Revision", "0")
	staleRouteResponse := httptest.NewRecorder()
	mux.ServeHTTP(staleRouteResponse, staleRoute)
	if staleRouteResponse.Code != http.StatusConflict || mutations != 3 {
		t.Fatalf("stale update status=%d mutations=%d body=%s", staleRouteResponse.Code, mutations, staleRouteResponse.Body.String())
	}

	listOther := httptest.NewRequest(http.MethodGet, "/v1/credentials/slots", nil)
	listOther.Header.Set("Authorization", "Bearer "+second.RawToken)
	listOtherResponse := httptest.NewRecorder()
	mux.ServeHTTP(listOtherResponse, listOther)
	if listOtherResponse.Code != http.StatusOK || strings.Contains(listOtherResponse.Body.String(), created.ID) {
		t.Fatalf("principal isolation failed: status=%d body=%s", listOtherResponse.Code, listOtherResponse.Body.String())
	}
}
