package identity

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
)

func TestOnboardingClientKeepsCodeAndExchangeTokenOutOfURLs(t *testing.T) {
	const code = "onb1.aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa.abcdefghijklmnopqrstuvwxyzABCDEFGH"
	const token = "ex1.bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb.abcdefghijklmnopqrstuvwxyzABCDEFGH"
	server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if strings.Contains(r.URL.String(), code) || strings.Contains(r.URL.String(), token) {
			t.Fatal("onboarding credential leaked into URL")
		}
		switch {
		case r.Method == http.MethodPost && r.URL.Path == "/api/v1/onboarding-exchanges":
			var body map[string]string
			_ = json.NewDecoder(r.Body).Decode(&body)
			if body["onboarding_code"] != code || r.Header.Get("Authorization") != "" {
				t.Fatalf("unexpected claim request: body=%#v authorization=%q", body, r.Header.Get("Authorization"))
			}
			w.Header().Set("Cache-Control", "no-store")
			w.WriteHeader(http.StatusCreated)
			_, _ = w.Write([]byte(`{"exchange_id":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb","exchange_token":"` + token + `","expires_at":"2026-07-17T12:30:00Z","version":1}`))
		case r.Method == http.MethodPut && r.URL.Path == "/api/v1/onboarding-exchanges/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb/metadata":
			if r.Header.Get("Authorization") != "Bearer "+token || r.Header.Get("If-Match") != `W/"1"` || len(r.Header.Get("Idempotency-Key")) < 16 {
				t.Fatalf("unexpected exchange headers: %#v", r.Header)
			}
			w.Header().Set("ETag", `W/"2"`)
			w.WriteHeader(http.StatusAccepted)
			_, _ = w.Write([]byte(`{"exchange_id":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb","application_id":"cccccccc-cccc-cccc-cccc-cccccccccccc","status":"preparing","version":2,"expires_at":"2026-07-17T12:30:00Z"}`))
		default:
			http.NotFound(w, r)
		}
	}))
	defer server.Close()

	client, err := NewOnboardingClient(server.URL, server.Client(), "production")
	if err != nil {
		t.Fatal(err)
	}
	claimed, err := client.Claim(context.Background(), code)
	if err != nil {
		t.Fatal(err)
	}
	metadata := ConsumerMetadata{PublicBaseURL: "https://api.example.com", WebBaseURL: "https://gateway.example.com", RedirectURIs: []string{"https://api.example.com/api/v1/auth/oidc/callback"}, LogoutURIs: []string{"https://gateway.example.com/"}}
	view, err := client.SubmitMetadata(context.Background(), claimed, metadata, "pairing-metadata-123456")
	if err != nil || view.Status != ExchangePreparing || view.Version != 2 {
		t.Fatalf("view=%#v err=%v", view, err)
	}
}

func TestManifestV1ValidationRequiresStableFieldsAndCapabilityDependencies(t *testing.T) {
	valid := ManifestV1{
		SchemaVersion: 1, Issuer: "https://auth.example.com/issuer/shared",
		TenantID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", ApplicationID: "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
		Capabilities: []string{"oidc_login", "api_access", "machine_to_machine", "token_introspection", "session_revocation"},
		Audience:     "urn:easyai:resource:bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", Scopes: []string{"openid", "gateway.access"},
		Clients:        ManifestClients{BrowserLogin: &ManifestClient{ClientID: "browser"}, MachineToMachine: &ManifestClient{ClientID: "service"}},
		SecurityEvents: &ManifestSecurityEvents{TransmitterIssuer: "https://auth.example.com/ssf", ConfigurationEndpoint: "https://auth.example.com/.well-known/ssf-configuration/ssf", Audience: "urn:easyai:ssf:receiver:bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"},
	}
	if err := valid.Validate("production"); err != nil {
		t.Fatal(err)
	}
	invalid := valid
	invalid.Capabilities = []string{"session_revocation"}
	if err := invalid.Validate("production"); err == nil {
		t.Fatal("manifest with missing capability dependencies was accepted")
	}
	invalid = valid
	invalid.SchemaVersion = 2
	if err := invalid.Validate("production"); err == nil {
		t.Fatal("unsupported manifest schema was accepted")
	}
}

func TestManifestV2ValidationRequiresExplicitMultiTenantRuntimeContract(t *testing.T) {
	valid := ManifestV2{
		SchemaVersion: 2, TenantMode: "multi_tenant",
		Issuer: "https://auth.example.com/issuer/shared", ApplicationID: "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
		Capabilities: []string{"oidc_login", "api_access", "machine_to_machine", "token_introspection", "session_revocation"},
		Audience:     "urn:easyai:resource:bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", Scopes: []string{"openid", "gateway.access"},
		Clients: ManifestClients{
			BrowserLogin:     &ManifestClient{ClientID: "browser"},
			MachineToMachine: &ManifestClient{ClientID: "service"},
		},
		TenantContext: &ManifestTenantContext{
			Endpoint: "https://auth.example.com/api/v1/runtime/tenants/{tenantId}",
			Audience: "urn:easyai:auth-center:tenant-context",
			Scope:    "tenant.context.read",
		},
		SecurityEvents: &ManifestSecurityEvents{
			TransmitterIssuer:     "https://auth.example.com/ssf",
			ConfigurationEndpoint: "https://auth.example.com/.well-known/ssf-configuration/ssf",
			Audience:              "urn:easyai:ssf:receiver:bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
		},
	}
	if err := valid.Validate("production"); err != nil {
		t.Fatal(err)
	}
	for _, mutate := range []func(*ManifestV2){
		func(value *ManifestV2) { value.TenantMode = "" },
		func(value *ManifestV2) { value.TenantID = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" },
		func(value *ManifestV2) { value.TenantContext = nil },
		func(value *ManifestV2) { value.TenantContext.Scope = "" },
		func(value *ManifestV2) { value.TenantContext.Audience = "urn:wrong" },
	} {
		candidate := valid
		contextCopy := *valid.TenantContext
		candidate.TenantContext = &contextCopy
		mutate(&candidate)
		if err := candidate.Validate("production"); err == nil {
			t.Fatalf("invalid Manifest V2 was accepted: %#v", candidate)
		}
	}
}

func TestOnboardingClientRejectsRedirects(t *testing.T) {
	target := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		http.Error(w, "should not be reached", http.StatusTeapot)
	}))
	defer target.Close()
	redirect := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		http.Redirect(w, r, target.URL, http.StatusTemporaryRedirect)
	}))
	defer redirect.Close()
	client, err := NewOnboardingClient(redirect.URL, redirect.Client(), "production")
	if err != nil {
		t.Fatal(err)
	}
	if _, err := client.Claim(context.Background(), "onb1.aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa.abcdefghijklmnopqrstuvwxyzABCDEFGH"); err == nil {
		t.Fatal("redirecting onboarding endpoint was accepted")
	}
}

func TestManifestAllowsLoopbackHTTPOnlyInLocalEnvironments(t *testing.T) {
	manifest := ManifestV1{
		SchemaVersion: 1, Issuer: "https://auth.example.com/issuer/easyai",
		TenantID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", ApplicationID: "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
		Capabilities: []string{"machine_to_machine", "token_introspection", "session_revocation"}, Scopes: []string{"gateway.access"},
		Clients: ManifestClients{MachineToMachine: &ManifestClient{ClientID: "service"}},
		SecurityEvents: &ManifestSecurityEvents{
			TransmitterIssuer: "https://auth.example.com/ssf", ConfigurationEndpoint: "https://auth.example.com/.well-known/ssf-configuration/ssf",
			Audience: "urn:easyai:ssf:receiver:bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
		},
	}
	for _, test := range []struct {
		name   string
		mutate func(*ManifestV1)
	}{
		{name: "OIDC issuer", mutate: func(value *ManifestV1) { value.Issuer = "http://localhost:18003/issuer/easyai" }},
		{name: "SSF issuer", mutate: func(value *ManifestV1) { value.SecurityEvents.TransmitterIssuer = "http://127.0.0.1:18004/ssf" }},
		{name: "SSF configuration", mutate: func(value *ManifestV1) {
			value.SecurityEvents.ConfigurationEndpoint = "http://127.0.0.1:18004/.well-known/ssf-configuration/ssf"
		}},
	} {
		t.Run(test.name, func(t *testing.T) {
			candidate := manifest
			securityEvents := *manifest.SecurityEvents
			candidate.SecurityEvents = &securityEvents
			test.mutate(&candidate)
			if err := candidate.Validate("production"); err == nil {
				t.Fatalf("production accepted loopback HTTP %s URL", test.name)
			}
		})
	}
	localManifest := manifest
	localSecurityEvents := *manifest.SecurityEvents
	localManifest.SecurityEvents = &localSecurityEvents
	localManifest.Issuer = "http://localhost:18003/issuer/easyai"
	localManifest.SecurityEvents.TransmitterIssuer = "http://127.0.0.1:18004/ssf"
	localManifest.SecurityEvents.ConfigurationEndpoint = "http://127.0.0.1:18004/.well-known/ssf-configuration/ssf"
	if err := localManifest.Validate("test"); err != nil {
		t.Fatalf("test environment rejected loopback HTTP manifest URLs: %v", err)
	}
	localManifest.Capabilities = []string{"machine_to_machine"}
	localManifest.Issuer = manifest.Issuer
	if err := localManifest.Validate("production"); err == nil {
		t.Fatal("production accepted optional loopback HTTP security event metadata")
	}
}

func TestOnboardingClientAllowsLoopbackHTTPOnlyInLocalEnvironments(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
	defer server.Close()
	if _, err := NewOnboardingClient(server.URL, server.Client(), "production"); err == nil {
		t.Fatal("production accepted loopback HTTP Auth Center URL")
	}
	if _, err := NewOnboardingClient(server.URL, server.Client(), "development"); err != nil {
		t.Fatalf("development rejected loopback HTTP Auth Center URL: %v", err)
	}
}
