package httpapi import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "supply-intelligence/internal/discovery" "supply-intelligence/internal/domain" "supply-intelligence/internal/gatewayconsumer" "supply-intelligence/internal/probe" "supply-intelligence/internal/publish" "supply-intelligence/internal/repository" ) func TestServerRoutingStateEndpoint(t *testing.T) { repo := repository.NewMemoryRepository() repo.UpsertRoutingState(domain.AccountRoutingState{ AccountID: 101, Platform: "openai", AccountStatus: domain.AccountStatusActive, RoutingEnabled: true, RiskScore: 10, ReasonCode: "ok", LastProbeAt: time.Unix(100, 0).UTC(), Version: 3, }) server := NewServer(repo, probe.NewService(repo), publish.NewService(repo), gatewayconsumer.NewService(repo), discovery.NewService(repo), nil) req := httptest.NewRequest(http.MethodGet, "/internal/supply-intelligence/accounts/101/routing-state", nil) rr := httptest.NewRecorder() server.Routes().ServeHTTP(rr, req) if rr.Code != http.StatusOK { t.Fatalf("unexpected status: %d body=%s", rr.Code, rr.Body.String()) } var got domain.AccountRoutingState if err := json.NewDecoder(rr.Body).Decode(&got); err != nil { t.Fatalf("decode error: %v", err) } if got.AccountID != 101 || got.AccountStatus != domain.AccountStatusActive { t.Fatalf("unexpected payload: %+v", got) } } func TestServerProbeEvaluateEndpointPaths(t *testing.T) { tests := []struct { name string body string wantStatus int wantClassification domain.ProbeClassification wantAccountStatus domain.AccountStatus wantReasonCode string wantRoutingEnabled bool }{ { name: "success", body: `{"account_id":201,"platform":"openai","current_status":"suspended","status_code":200}`, wantStatus: http.StatusOK, wantClassification: domain.ProbeClassificationSuccess, wantAccountStatus: domain.AccountStatusActive, wantReasonCode: "ok", wantRoutingEnabled: true, }, { name: "explicit_failure", body: `{"account_id":202,"platform":"openai","current_status":"active","status_code":401}`, wantStatus: http.StatusOK, wantClassification: domain.ProbeClassificationExplicitFailure, wantAccountStatus: domain.AccountStatusSuspended, wantReasonCode: "auth_rejected", wantRoutingEnabled: false, }, { name: "inconclusive", body: `{"account_id":203,"platform":"openai","current_status":"suspended","transport_error":"dial tcp timeout"}`, wantStatus: http.StatusOK, wantClassification: domain.ProbeClassificationInconclusive, wantAccountStatus: domain.AccountStatusSuspended, wantReasonCode: "transport_error", wantRoutingEnabled: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { repo := repository.NewMemoryRepository() server := NewServer(repo, probe.NewService(repo), publish.NewService(repo), gatewayconsumer.NewService(repo), discovery.NewService(repo), nil) req := httptest.NewRequest(http.MethodPost, "/internal/supply-intelligence/probe/evaluate", bytes.NewBufferString(tt.body)) rr := httptest.NewRecorder() server.Routes().ServeHTTP(rr, req) if rr.Code != tt.wantStatus { t.Fatalf("unexpected status: %d body=%s", rr.Code, rr.Body.String()) } var got probe.EvaluateOutput if err := json.NewDecoder(rr.Body).Decode(&got); err != nil { t.Fatalf("decode error: %v", err) } if got.Classification != tt.wantClassification { t.Fatalf("unexpected classification: %q", got.Classification) } if got.RoutingState.AccountStatus != tt.wantAccountStatus { t.Fatalf("unexpected account status: %q", got.RoutingState.AccountStatus) } if got.RoutingState.ReasonCode != tt.wantReasonCode { t.Fatalf("unexpected reason code: %q", got.RoutingState.ReasonCode) } if got.RoutingState.RoutingEnabled != tt.wantRoutingEnabled { t.Fatalf("unexpected routing enabled: %v", got.RoutingState.RoutingEnabled) } }) } } func TestServerPublishPackageEventEndpoint(t *testing.T) { repo := repository.NewMemoryRepository() server := NewServer(repo, probe.NewService(repo), publish.NewService(repo), gatewayconsumer.NewService(repo), discovery.NewService(repo), nil) body := bytes.NewBufferString(`{"event_id":"evt-1","package_id":1001,"platform":"openai","model":"gpt-4.1-mini","version":7,"occurred_at":"2026-05-06T20:30:00Z"}`) req := httptest.NewRequest(http.MethodPost, "/internal/supply-intelligence/publish/package-event", body) rr := httptest.NewRecorder() server.Routes().ServeHTTP(rr, req) if rr.Code != http.StatusOK { t.Fatalf("unexpected publish status: %d body=%s", rr.Code, rr.Body.String()) } var event domain.PackageChangeEvent if err := json.NewDecoder(rr.Body).Decode(&event); err != nil { t.Fatalf("decode error: %v", err) } if event.EventID != "evt-1" || event.EventType != publish.PackagePublishedEventType { t.Fatalf("unexpected event: %+v", event) } if event.GatewaySyncStatus != domain.GatewaySyncStatusPending { t.Fatalf("unexpected sync status: %q", event.GatewaySyncStatus) } } func TestServerPackageChangeListAndAck(t *testing.T) { repo := repository.NewMemoryRepository() repo.AppendPackageEvent(domain.PackageChangeEvent{EventID: "evt-1", EventType: publish.PackagePublishedEventType, PackageID: 1001, Platform: "openai", Model: "gpt-4.1-mini", OccurredAt: time.Unix(5, 0).UTC(), Version: 7, GatewaySyncStatus: domain.GatewaySyncStatusPending}) server := NewServer(repo, probe.NewService(repo), publish.NewService(repo), gatewayconsumer.NewService(repo), discovery.NewService(repo), nil) listReq := httptest.NewRequest(http.MethodGet, "/internal/supply-intelligence/gateway/package-changes", nil) listRR := httptest.NewRecorder() server.Routes().ServeHTTP(listRR, listReq) if listRR.Code != http.StatusOK { t.Fatalf("unexpected list status: %d body=%s", listRR.Code, listRR.Body.String()) } var listResp struct { Items []domain.PackageChangeEvent `json:"items"` NextCursor string `json:"next_cursor"` } if err := json.NewDecoder(listRR.Body).Decode(&listResp); err != nil { t.Fatalf("decode list error: %v", err) } if len(listResp.Items) != 1 || listResp.NextCursor != "1" { t.Fatalf("unexpected list response: %+v", listResp) } ackReq := httptest.NewRequest(http.MethodPost, "/internal/supply-intelligence/gateway/package-changes/evt-1/ack", bytes.NewBufferString(`{"consumer":"gateway","result":"applied","detail":"ok"}`)) ackRR := httptest.NewRecorder() server.Routes().ServeHTTP(ackRR, ackReq) if ackRR.Code != http.StatusNoContent { t.Fatalf("unexpected ack status: %d body=%s", ackRR.Code, ackRR.Body.String()) } updated, _ := repo.ListPackageEventsAfter("") if len(updated) != 1 || updated[0].GatewaySyncStatus != domain.GatewaySyncStatusApplied { t.Fatalf("unexpected ack state: %+v", updated) } } func TestServerPackageChangeListWithCursor(t *testing.T) { repo := repository.NewMemoryRepository() repo.AppendPackageEvent(domain.PackageChangeEvent{EventID: "evt-1", EventType: publish.PackagePublishedEventType, PackageID: 1001, Platform: "openai", Model: "gpt-4.1-mini", OccurredAt: time.Unix(5, 0).UTC(), Version: 7, GatewaySyncStatus: domain.GatewaySyncStatusPending}) repo.AppendPackageEvent(domain.PackageChangeEvent{EventID: "evt-2", EventType: publish.PackagePublishedEventType, PackageID: 1002, Platform: "openai", Model: "gpt-4.1", OccurredAt: time.Unix(6, 0).UTC(), Version: 8, GatewaySyncStatus: domain.GatewaySyncStatusPending}) server := NewServer(repo, probe.NewService(repo), publish.NewService(repo), gatewayconsumer.NewService(repo), discovery.NewService(repo), nil) req := httptest.NewRequest(http.MethodGet, "/internal/supply-intelligence/gateway/package-changes?cursor=1", nil) rr := httptest.NewRecorder() server.Routes().ServeHTTP(rr, req) if rr.Code != http.StatusOK { t.Fatalf("unexpected status: %d body=%s", rr.Code, rr.Body.String()) } var resp struct { Items []domain.PackageChangeEvent `json:"items"` NextCursor string `json:"next_cursor"` } if err := json.NewDecoder(rr.Body).Decode(&resp); err != nil { t.Fatalf("decode error: %v", err) } if len(resp.Items) != 1 || resp.Items[0].EventID != "evt-2" || resp.NextCursor != "2" { t.Fatalf("unexpected cursor response: %+v", resp) } } func TestServerConsumeOnceEndpoint(t *testing.T) { repo := repository.NewMemoryRepository() repo.AppendPackageEvent(domain.PackageChangeEvent{EventID: "evt-apply", EventType: publish.PackagePublishedEventType, PackageID: 1001, Platform: "openai", Model: "gpt-4.1-mini", OccurredAt: time.Unix(5, 0).UTC(), Version: 7, GatewaySyncStatus: domain.GatewaySyncStatusPending}) repo.AppendPackageEvent(domain.PackageChangeEvent{EventID: "evt-fail", EventType: publish.PackagePublishedEventType, PackageID: 1002, Platform: "openai", Model: "gpt-fail-model", OccurredAt: time.Unix(6, 0).UTC(), Version: 8, GatewaySyncStatus: domain.GatewaySyncStatusPending}) server := NewServer(repo, probe.NewService(repo), publish.NewService(repo), gatewayconsumer.NewService(repo), discovery.NewService(repo), nil) req := httptest.NewRequest(http.MethodPost, "/internal/supply-intelligence/gateway/consume-once", bytes.NewBufferString(`{"consumer":"gateway"}`)) rr := httptest.NewRecorder() server.Routes().ServeHTTP(rr, req) if rr.Code != http.StatusOK { t.Fatalf("unexpected consume status: %d body=%s", rr.Code, rr.Body.String()) } var out gatewayconsumer.ConsumeOnceOutput if err := json.NewDecoder(rr.Body).Decode(&out); err != nil { t.Fatalf("decode error: %v", err) } if len(out.Items) != 2 { t.Fatalf("unexpected consume output length: %+v", out) } if out.Items[0].Result != domain.GatewayAckResultApplied || out.Items[0].GatewaySyncStatus != domain.GatewaySyncStatusApplied || out.Items[0].Detail == "" { t.Fatalf("unexpected first consume item: %+v", out.Items[0]) } if out.Items[1].Result != domain.GatewayAckResultFailed || out.Items[1].GatewaySyncStatus != domain.GatewaySyncStatusFailed || out.Items[1].Detail == "" { t.Fatalf("unexpected second consume item: %+v", out.Items[1]) } } func TestServerDiscoveryCandidateCreateAndList(t *testing.T) { repo := repository.NewMemoryRepository() server := NewServer(repo, probe.NewService(repo), publish.NewService(repo), gatewayconsumer.NewService(repo), discovery.NewService(repo), nil) createReq := httptest.NewRequest(http.MethodPost, "/internal/supply-intelligence/discovery/candidates", bytes.NewBufferString(`{"candidate_id":"cand-1","account_id":301,"platform":"openai","model":"gpt-4.1-mini","source":"manual_seed","reason_code":"new_model","discovered_at":"2026-05-06T20:30:00Z"}`)) createRR := httptest.NewRecorder() server.Routes().ServeHTTP(createRR, createReq) if createRR.Code != http.StatusOK { t.Fatalf("unexpected create status: %d body=%s", createRR.Code, createRR.Body.String()) } listReq := httptest.NewRequest(http.MethodGet, "/internal/supply-intelligence/discovery/candidates?status=pending_admission", nil) listRR := httptest.NewRecorder() server.Routes().ServeHTTP(listRR, listReq) if listRR.Code != http.StatusOK { t.Fatalf("unexpected list status: %d body=%s", listRR.Code, listRR.Body.String()) } var listResp struct { Items []domain.DiscoveryCandidate `json:"items"` } if err := json.NewDecoder(listRR.Body).Decode(&listResp); err != nil { t.Fatalf("decode list error: %v", err) } if len(listResp.Items) != 1 || listResp.Items[0].CandidateID != "cand-1" || listResp.Items[0].Status != domain.DiscoveryCandidateStatusPendingAdmission { t.Fatalf("unexpected discovery list response: %+v", listResp.Items) } } func TestServerDiscoveryCandidateRejectsInvalidInput(t *testing.T) { repo := repository.NewMemoryRepository() server := NewServer(repo, probe.NewService(repo), publish.NewService(repo), gatewayconsumer.NewService(repo), discovery.NewService(repo), nil) req := httptest.NewRequest(http.MethodPost, "/internal/supply-intelligence/discovery/candidates", bytes.NewBufferString(`{"candidate_id":"","account_id":0}`)) rr := httptest.NewRecorder() server.Routes().ServeHTTP(rr, req) if rr.Code != http.StatusBadRequest { t.Fatalf("unexpected status: %d body=%s", rr.Code, rr.Body.String()) } }