package authorizationv1

import (
	"context"
	"encoding/json"
	"errors"
	"strings"
	"testing"
	"time"
)

func authorizationDeadline(t *testing.T) context.Context {
	t.Helper()
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	t.Cleanup(cancel)
	return ctx
}

func validBeginRequest() BeginRequest {
	return BeginRequest{AppInstallationID: "installation-1", ClientID: "public-client",
		RedirectURI: "https://client.example/callback", ResponseType: "code", Scope: "api",
		State: "state-1", CodeChallenge: "challenge", CodeChallengeMethod: "S256",
		Issuer: "https://crm.example"}
}

func TestAuthorizationRequestsPreserveOptionalResourcePresence(t *testing.T) {
	ctx := authorizationDeadline(t)
	request := validBeginRequest()
	request.ResourcePresent = true
	if err := ValidateBeginRequest(ctx, request); err != nil {
		t.Fatalf("explicit empty resource rejected before OAuth policy: %v", err)
	}
	request.Resource = "https://resource.example"
	if err := ValidateBeginRequest(ctx, request); err != nil {
		t.Fatal(err)
	}
	request.ResourcePresent = false
	if err := ValidateBeginRequest(ctx, request); err == nil {
		t.Fatal("resource value without presence marker accepted")
	}
}

func TestAuthorizationProtocolResultsAndSecretsAreClosed(t *testing.T) {
	result := BeginResponse{ProtocolError: &ProtocolError{Code: "invalid_scope", SafeDescription: "scope is not allowed",
		TrustedRedirectURI: "https://client.example/callback", State: "state-1"}}
	if err := ValidateBeginResponse(result); err != nil {
		t.Fatal(err)
	}
	result.ProtocolError.Code = "invalid_client"
	result.ProtocolError.TrustedRedirectURI = ""
	result.ProtocolError.State = ""
	if err := ValidateBeginResponse(result); err != nil {
		t.Fatalf("safe non-redirecting invalid_client rejected: %v", err)
	}
	result.ProtocolError.TrustedRedirectURI = "https://client.example/callback"
	result.ProtocolError.State = "state-1"
	result.ProtocolError.Code = "server_error"
	if err := ValidateBeginResponse(result); err != nil {
		t.Fatalf("standard authorization server_error rejected: %v", err)
	}
	result.ProtocolError.Code = "future_error"
	if err := ValidateBeginResponse(result); err == nil {
		t.Fatal("unknown authorization error accepted")
	}
	exchangeError := ExchangeResponse{ProtocolError: &ProtocolError{Code: "invalid_grant", SafeDescription: "grant is invalid"}}
	if err := ValidateExchangeResponse(exchangeError); err != nil {
		t.Fatalf("standard exchange error rejected: %v", err)
	}
	exchangeError.ProtocolError.Code = "access_denied"
	if err := ValidateExchangeResponse(exchangeError); err == nil {
		t.Fatal("authorization-only error accepted by exchange")
	}
	exchangeError.ProtocolError.Code = "invalid_grant"
	exchangeError.ProtocolError.TrustedRedirectURI = "https://client.example/callback"
	if err := ValidateExchangeResponse(exchangeError); err == nil {
		t.Fatal("redirect-bearing exchange error accepted")
	}
	if err := ValidateExchangeResponse(ExchangeResponse{Token: &Token{TokenType: "Bearer", ExpiresInSecond: 3600,
		AccessTokenIssued: true, RefreshTokenIssued: true}}); err != nil {
		t.Fatal(err)
	}
}

func TestAuthorizationOversizedInstallationIsResourceExhausted(t *testing.T) {
	request := validBeginRequest()
	request.AppInstallationID = strings.Repeat("i", MaxIdentifierBytes+1)
	var failure *Failure
	if err := ValidateBeginRequest(authorizationDeadline(t), request); !errors.As(err, &failure) ||
		failure.Category != FailureResourceExhausted || failure.LimitKind != LimitAppInstallationID ||
		failure.Maximum != MaxIdentifierBytes {
		t.Fatalf("oversized installation failure = %#v, %v", failure, err)
	}
}

func TestAuthorizationJSONUsesStableSnakeCase(t *testing.T) {
	raw, err := json.Marshal(BeginRequest{AppInstallationID: "installation-1", ResourcePresent: true})
	if err != nil {
		t.Fatal(err)
	}
	encoded := string(raw)
	if !strings.Contains(encoded, `"app_installation_id":"installation-1"`) ||
		!strings.Contains(encoded, `"resource_present":true`) || strings.Contains(encoded, "AppInstallationID") {
		t.Fatalf("unstable JSON shape: %s", raw)
	}
}

func TestAuthorizationRequestsRequireDeadlineAndClosedGrantProfile(t *testing.T) {
	if err := ValidateBeginRequest(context.Background(), validBeginRequest()); err == nil {
		t.Fatal("request without deadline accepted")
	}
	exchange := ExchangeRequest{AppInstallationID: "installation-1", GrantType: GrantTypeAuthorizationCode,
		ClientID: "public-client", RedirectURI: "https://client.example/callback", Issuer: "https://crm.example"}
	if err := ValidateExchangeRequest(authorizationDeadline(t), exchange); err != nil {
		t.Fatal(err)
	}
	exchange.GrantType = GrantType("future")
	var failure *Failure
	if err := ValidateExchangeRequest(authorizationDeadline(t), exchange); !errors.As(err, &failure) || failure.InvalidComponent != InvalidComponentOperation {
		t.Fatalf("future grant failure = %#v, %v", failure, err)
	}
}

func TestAuthorizationClientCredentialsExchangeIsNeutral(t *testing.T) {
	exchange := ExchangeRequest{
		AppInstallationID: "installation-1",
		GrantType:         GrantTypeClientCredentials,
		ClientID:          "machine-client",
		RequestedScope:    "api",
		Issuer:            "https://crm.example",
		Resource:          "https://resource.example",
		ResourcePresent:   true,
	}
	if err := ValidateExchangeRequest(authorizationDeadline(t), exchange); err != nil {
		t.Fatalf("client credentials exchange rejected: %v", err)
	}
}

func TestAuthorizationClientCredentialsRejectsRedirectURI(t *testing.T) {
	exchange := ExchangeRequest{
		AppInstallationID: "installation-1",
		GrantType:         GrantTypeClientCredentials,
		ClientID:          "machine-client",
		RequestedScope:    "api",
		Issuer:            "https://crm.example",
		RedirectURI:       "https://client.example/callback",
	}
	var failure *Failure
	if err := ValidateExchangeRequest(authorizationDeadline(t), exchange); !errors.As(err, &failure) ||
		failure.InvalidComponent != InvalidComponentOperation {
		t.Fatalf("redirect URI failure = %#v, %v", failure, err)
	}
}

func TestAuthorizationClientCredentialsCannotIssueRefreshToken(t *testing.T) {
	request := ExchangeRequest{GrantType: GrantTypeClientCredentials}
	response := ExchangeResponse{Token: &Token{
		TokenType:          "Bearer",
		ExpiresInSecond:    3600,
		AccessTokenIssued:  true,
		RefreshTokenIssued: true,
	}}
	var failure *Failure
	if err := ValidateExchangeResponseForRequest(request, response); !errors.As(err, &failure) ||
		failure.Category != FailureInternal {
		t.Fatalf("refresh token issuance failure = %#v, %v", failure, err)
	}
	response.Token.RefreshTokenIssued = false
	if err := ValidateExchangeResponseForRequest(request, response); err != nil {
		t.Fatalf("access-only token rejected: %v", err)
	}
	request.GrantType = GrantType("future")
	if err := ValidateExchangeResponseForRequest(request, response); err == nil {
		t.Fatal("response for unknown grant accepted")
	}
}

func TestAuthorizationRequestsPreserveMissingOAuthFieldsForOwnerClassification(t *testing.T) {
	ctx := authorizationDeadline(t)
	begin := validBeginRequest()
	begin.ClientID, begin.RedirectURI, begin.ResponseType = "", "", ""
	if err := ValidateBeginRequest(ctx, begin); err != nil {
		t.Fatalf("missing OAuth begin fields were preempted by transport validation: %v", err)
	}
	exchange := ExchangeRequest{AppInstallationID: "installation-1", GrantType: GrantTypeAuthorizationCode,
		Issuer: "https://crm.example"}
	if err := ValidateExchangeRequest(ctx, exchange); err != nil {
		t.Fatalf("missing OAuth exchange fields were preempted by transport validation: %v", err)
	}
}
