package sessionv1

import (
	"context"
	"errors"
	"strconv"
	"testing"
	"time"
)

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

func validCreateRequest() CreateRequest {
	return CreateRequest{
		AppInstallationID: "installation-1", Method: AuthenticationMethodPassword,
		ApplicationClientID: "nexuscrm-ui", RequestedScope: "api refresh_token",
		SourceAddress: "192.0.2.1", UserAgent: "browser", IssueControlProof: true,
	}
}

func validSession() Session {
	return Session{Principal: Principal{
		PrincipalID: "user-1", DisplayName: "Ada Lovelace", Email: "ada@example.test",
		ProfileID: "profile-1", RoleID: "role-1",
	}, ExpiresAtUnixSecond: 42}
}

func TestCreateRequestProfilesAreClosed(t *testing.T) {
	ctx := deadlineContext(t)
	if err := ValidateCreateRequest(ctx, validCreateRequest()); err != nil {
		t.Fatal(err)
	}
	federated := validCreateRequest()
	federated.Method = AuthenticationMethodFederatedIDToken
	federated.ApplicationClientID = ""
	federated.RequestedScope = ""
	federated.FederatedProviderID = "google"
	if err := ValidateCreateRequest(ctx, federated); err != nil {
		t.Fatal(err)
	}
	invalidRequests := []CreateRequest{
		{},
		{AppInstallationID: "installation-1", Method: AuthenticationMethodPassword},
		{AppInstallationID: "installation-1", Method: AuthenticationMethodPassword, ApplicationClientID: "client", FederatedProviderID: "provider"},
		{AppInstallationID: "installation-1", Method: AuthenticationMethodFederatedIDToken, FederatedProviderID: "provider", ApplicationClientID: "client"},
		{AppInstallationID: "installation-1", Method: "future"},
	}
	for index, request := range invalidRequests {
		if err := ValidateCreateRequest(ctx, request); err == nil {
			t.Fatalf("request[%d] accepted: %#v", index, request)
		}
	}
}

func TestRequestsRequireDeadlineAndCanonicalValues(t *testing.T) {
	if err := ValidateCreateRequest(context.Background(), validCreateRequest()); err == nil {
		t.Fatal("request without deadline accepted")
	}
	request := validCreateRequest()
	request.UserAgent = " bad\nagent "
	if err := ValidateCreateRequest(deadlineContext(t), request); err == nil {
		t.Fatal("noncanonical user agent accepted")
	}
	if err := ValidateReadRequest(deadlineContext(t), ReadRequest{AppInstallationID: "installation-1", UseControlProof: true}); err == nil {
		t.Fatal("control proof without renewal accepted")
	}
}

func TestResponsesRequireCompletePrincipalAndTransportReceipt(t *testing.T) {
	response := CreateResponse{Session: validSession(), AccessTokenIssued: true, ControlProofIssued: true}
	if err := ValidateCreateResponse(validCreateRequest(), response); err != nil {
		t.Fatal(err)
	}
	response.AccessTokenIssued = false
	if err := ValidateCreateResponse(validCreateRequest(), response); err == nil {
		t.Fatal("response without access-token receipt accepted")
	}
	response.AccessTokenIssued = true
	withoutControlIntent := validCreateRequest()
	withoutControlIntent.IssueControlProof = false
	if err := ValidateCreateResponse(withoutControlIntent, response); err == nil {
		t.Fatal("unrequested control proof accepted")
	}
	read := ReadResponse{Session: validSession(), ReplacementAccessTokenIssued: true, ControlProofExpiresAtUnixSecond: 42}
	readRequest := ReadRequest{AppInstallationID: "installation-1", AllowRenewal: true, UseControlProof: true}
	if err := ValidateReadResponse(readRequest, read); err != nil {
		t.Fatal(err)
	}
	withoutEmail := read
	withoutEmail.Session.Principal.Email = ""
	if err := ValidateReadResponse(readRequest, withoutEmail); err != nil {
		t.Fatalf("existing session without optional email rejected: %v", err)
	}
	withInvalidEmail := read
	withInvalidEmail.Session.Principal.Email = " bad@example.test "
	if err := ValidateReadResponse(readRequest, withInvalidEmail); err == nil {
		t.Fatal("noncanonical optional email accepted")
	}
	read.ReplacementAccessTokenIssued = false
	if err := ValidateReadResponse(readRequest, read); err == nil {
		t.Fatal("control-proof expiry without replacement bearer accepted")
	}
	read = ReadResponse{Session: validSession(), ReplacementAccessTokenIssued: true}
	if err := ValidateReadResponse(readRequest, read); err == nil {
		t.Fatal("controlled replacement bearer without proof expiry accepted")
	}
	read = ReadResponse{Session: validSession(), ReplacementAccessTokenIssued: true}
	if err := ValidateReadResponse(ReadRequest{AppInstallationID: "installation-1"}, read); err == nil {
		t.Fatal("replacement bearer without renewal intent accepted")
	}
}

func TestRetryDispositionAndFailureSurface(t *testing.T) {
	if got := RetryDispositionFor(FailureStaleState); got != RetryAfterStateRefresh {
		t.Fatalf("stale retry = %q", got)
	}
	if got := RetryDispositionFor(FailureUnavailable); got != RetryWithBackoff {
		t.Fatalf("unavailable retry = %q", got)
	}
	if !ValidTopLevelFailure(FailureUnauthenticated) || ValidTopLevelFailure("future") {
		t.Fatal("failure surface is not closed")
	}
	var failure *Failure
	err := ValidateCreateRequest(nil, validCreateRequest())
	if !errors.As(err, &failure) || failure.InvalidComponent != InvalidComponentDeadline {
		t.Fatalf("nil context failure = %#v, %v", failure, err)
	}
}

func TestDocumentPolicyRequiresExplicitBoundedDecision(t *testing.T) {
	request := ReadRequest{AppInstallationID: "installation-1", DocumentPolicy: &DocumentPolicyRequest{RouteFamilyPrefix: "/extensions/data/", LinkRoutes: []DocumentLinkRoute{{Link: "identity", Method: "GET", Path: "/identity/:userId"}}}}
	if err := ValidateReadRequest(deadlineContext(t), request); err != nil {
		t.Fatal(err)
	}
	for _, policy := range []*DocumentPolicy{nil, {HourlyLimit: 1}, {HourlyLimit: -1, DailyLimit: -1}, {AllowedLinks: []string{"identity"}}, {Restricted: true, AllowedLinks: []string{"identity", "identity"}}} {
		if err := ValidateReadResponse(request, ReadResponse{Session: validSession(), DocumentPolicy: policy}); err == nil {
			t.Fatalf("accepted incomplete/invalid policy: %+v", policy)
		}
	}
	for _, policy := range []*DocumentPolicy{{}, {Restricted: true}, {Restricted: true, AllowedLinks: []string{"identity"}, HourlyLimit: 100, DailyLimit: 1000}} {
		if err := ValidateReadResponse(request, ReadResponse{Session: validSession(), DocumentPolicy: policy}); err != nil {
			t.Fatal(err)
		}
	}
	for _, path := range []string{"", "relative", "//host/path", "/data?scope=full", "/data/../admin"} {
		request.DocumentPolicy.RouteFamilyPrefix = path
		if err := ValidateReadRequest(deadlineContext(t), request); err == nil {
			t.Fatalf("accepted route family %q", path)
		}
	}
	request.DocumentPolicy.RouteFamilyPrefix = "/extensions/data/"
	request.DocumentPolicy.LinkRoutes = make([]DocumentLinkRoute, MaxDocumentLinkRoutes)
	for i := range request.DocumentPolicy.LinkRoutes {
		request.DocumentPolicy.LinkRoutes[i] = DocumentLinkRoute{Link: "resource-" + strconv.Itoa(i), Method: "GET", Path: "/identity/:userId"}
	}
	if err := ValidateReadRequest(deadlineContext(t), request); err != nil {
		t.Fatalf("rejected maximum route facts: %v", err)
	}
	request.DocumentPolicy.LinkRoutes = append(request.DocumentPolicy.LinkRoutes, request.DocumentPolicy.LinkRoutes[0])
	err := ValidateReadRequest(deadlineContext(t), request)
	var failure *Failure
	if !errors.As(err, &failure) || failure.Category != FailureResourceExhausted || failure.LimitKind != LimitOperationCount || failure.Maximum != MaxDocumentLinkRoutes {
		t.Fatalf("route count failure = %#v, %v", failure, err)
	}
}

func TestDocumentPolicyAcceptsAlternativeRoutesForOneVisibleResource(t *testing.T) {
	request := ReadRequest{AppInstallationID: "installation-1", DocumentPolicy: &DocumentPolicyRequest{
		RouteFamilyPrefix: "/extensions/data/",
		LinkRoutes: []DocumentLinkRoute{
			{Link: "identity", Method: "GET", Path: "/identity/:userId"},
			{Link: "identity", Method: "GET", Path: "/extensions/data/:version/identity/:userId"},
		},
	}}
	if err := ValidateReadRequest(deadlineContext(t), request); err != nil {
		t.Fatalf("alternative coverage routes rejected: %v", err)
	}
	response := ReadResponse{Session: validSession(), DocumentPolicy: &DocumentPolicy{Restricted: true, AllowedLinks: []string{"identity"}}}
	if err := ValidateReadResponse(request, response); err != nil {
		t.Fatalf("single visible resource decision rejected: %v", err)
	}
	response.DocumentPolicy.AllowedLinks = append(response.DocumentPolicy.AllowedLinks, "identity")
	if err := ValidateReadResponse(request, response); err == nil {
		t.Fatal("duplicate output key accepted")
	}
}

func TestDocumentPolicyAllowsRegistryKeysBeyondSupplementalPredicates(t *testing.T) {
	request := ReadRequest{AppInstallationID: "installation-1", DocumentPolicy: &DocumentPolicyRequest{RouteFamilyPrefix: "/extensions/data/", LinkRoutes: []DocumentLinkRoute{{Link: "identity", Method: "GET", Path: "/identity/:userId"}}}}
	response := ReadResponse{Session: validSession(), DocumentPolicy: &DocumentPolicy{Restricted: true, AllowedLinks: []string{"records"}}}
	if err := ValidateReadResponse(request, response); err != nil {
		t.Fatalf("registry-derived key rejected because it has no supplemental predicate: %v", err)
	}
}
