package federatedloginv1

import (
	"context"
	"errors"
	"fmt"
	"reflect"
	"testing"
	"time"

	common "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commonv1"
)

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

func federatedLoginTestState(t *testing.T) string {
	t.Helper()
	return "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8"
}

func TestFederatedLoginTransactionContractAcceptsTheMinimalNeutralJourney(t *testing.T) {
	ctx := federatedLoginTestContext(t)
	state := federatedLoginTestState(t)
	begin := BeginRequest{
		AppInstallationID: "installation-1",
		RedirectURI:       "https://app.example.test/oauth/callback",
	}
	if err := ValidateBeginRequest(ctx, begin); err != nil {
		t.Fatalf("ValidateBeginRequest: %v", err)
	}
	if err := ValidateBeginResponse(BeginResponse{
		State:               state,
		ExpiresAtUnixSecond: time.Now().Add(time.Minute).Unix(),
	}); err != nil {
		t.Fatalf("ValidateBeginResponse: %v", err)
	}
	if err := ValidateConsumeRequest(ctx, ConsumeRequest{
		AppInstallationID: begin.AppInstallationID,
		RedirectURI:       begin.RedirectURI,
		State:             state,
	}); err != nil {
		t.Fatalf("ValidateConsumeRequest: %v", err)
	}
	if err := ValidateConsumeResponse(ConsumeResponse{}); err != nil {
		t.Fatalf("ValidateConsumeResponse: %v", err)
	}
}

func TestFederatedLoginTransactionContractRejectsUnboundOrUnscopedState(t *testing.T) {
	ctx := federatedLoginTestContext(t)
	state := federatedLoginTestState(t)
	for name, request := range map[string]BeginRequest{
		"missing installation": {RedirectURI: "https://app.example.test/oauth/callback"},
		"missing redirect":     {AppInstallationID: "installation-1"},
		"untrimmed redirect": {
			AppInstallationID: "installation-1",
			RedirectURI:       " https://app.example.test/oauth/callback",
		},
	} {
		t.Run("begin "+name, func(t *testing.T) {
			if err := ValidateBeginRequest(ctx, request); err == nil {
				t.Fatalf("ValidateBeginRequest(%#v) succeeded", request)
			}
		})
	}
	for name, request := range map[string]ConsumeRequest{
		"missing installation": {
			RedirectURI: "https://app.example.test/oauth/callback", State: state,
		},
		"missing redirect": {AppInstallationID: "installation-1", State: state},
		"missing state": {
			AppInstallationID: "installation-1", RedirectURI: "https://app.example.test/oauth/callback",
		},
	} {
		t.Run("consume "+name, func(t *testing.T) {
			if err := ValidateConsumeRequest(ctx, request); err == nil {
				t.Fatalf("ValidateConsumeRequest(%#v) succeeded", request)
			}
		})
	}
	if err := ValidateBeginResponse(BeginResponse{}); err == nil {
		t.Fatal("empty begin response succeeded")
	}
	if err := ValidateBeginResponse(BeginResponse{
		State: "predictable-state", ExpiresAtUnixSecond: time.Now().Add(time.Minute).Unix(),
	}); err == nil {
		t.Fatal("non-generated state shape succeeded")
	}
}

func TestFederatedLoginStateShapeAcceptsCanonicalEntropyEncoding(t *testing.T) {
	if err := ValidateBeginResponse(BeginResponse{
		State: federatedLoginTestState(t), ExpiresAtUnixSecond: time.Now().Add(time.Minute).Unix(),
	}); err != nil {
		t.Fatalf("ValidateBeginResponse(canonical state): %v", err)
	}
}

func TestNormalizeConsumeOperationFailureMakesStateOutcomesIndistinguishable(t *testing.T) {
	stateOutcomes := map[string]error{
		"unknown": &Failure{
			Category: FailureMissing,
			Receipts: &structurallyValidButDisclosingReceipts,
		},
		"expired": &Failure{
			Category: FailureStaleState, StaleAxis: StaleAxisSecurity,
			RequiredRevision: 9, ServedRevision: 8,
		},
		"already used": &Failure{Category: FailureUnauthenticated, RequestID: "must-not-pass"},
		"redirect mismatch": &Failure{
			Category: FailureInvalidRequest, InvalidComponent: InvalidComponentFieldValue,
		},
		"authorization-shaped mismatch": &Failure{Category: FailureForbidden},
	}

	for name, source := range stateOutcomes {
		t.Run(name, func(t *testing.T) {
			normalized := NormalizeConsumeOperationFailure(fmt.Errorf("owner: %w", source))
			var failure *Failure
			if !errors.As(normalized, &failure) {
				t.Fatalf("normalized failure = %#v", normalized)
			}
			want := &Failure{Category: FailureUnauthenticated}
			if !reflect.DeepEqual(failure, want) {
				t.Fatalf("normalized failure = %#v, want %#v", failure, want)
			}
		})
	}
}

func TestFederatedLoginOperationFailureNormalizationUsesClosedGrammars(t *testing.T) {
	tests := []struct {
		name      string
		normalize func(error) error
		source    error
		want      *Failure
	}{
		{
			name:      "begin security refresh",
			normalize: NormalizeBeginOperationFailure,
			source: &Failure{
				Category: FailureStaleState, StaleAxis: StaleAxisSecurity,
				RequiredRevision: 9, ServedRevision: 8,
			},
			want: &Failure{
				Category: FailureStaleState, StaleAxis: StaleAxisSecurity,
				RequiredRevision: 9, ServedRevision: 8,
			},
		},
		{
			name:      "bare consume admission",
			normalize: NormalizeConsumeOperationFailure,
			source:    &Failure{Category: FailureResourceExhausted},
			want:      &Failure{Category: FailureResourceExhausted},
		},
		{
			name:      "consume admission retry instruction",
			normalize: NormalizeConsumeOperationFailure,
			source: &Failure{
				Category: FailureResourceExhausted, LimitKind: LimitAdmission,
				Maximum: 12, RetryAfterMillis: 50,
			},
			want: &Failure{
				Category: FailureResourceExhausted, LimitKind: LimitAdmission,
				Maximum: 12, RetryAfterMillis: 50,
			},
		},
		{
			name:      "invalid retry instruction fails closed",
			normalize: NormalizeConsumeOperationFailure,
			source:    &Failure{Category: FailureForbidden, RetryAfterMillis: 50},
			want:      &Failure{Category: FailureInternal},
		},
		{
			name:      "consume cancellation",
			normalize: NormalizeConsumeOperationFailure,
			source:    context.Canceled,
			want:      &Failure{Category: FailureCanceled},
		},
		{
			name:      "consume deadline",
			normalize: NormalizeConsumeOperationFailure,
			source:    context.DeadlineExceeded,
			want:      &Failure{Category: FailureDeadlineExceeded},
		},
		{
			name:      "consume unavailable",
			normalize: NormalizeConsumeOperationFailure,
			source:    &Failure{Category: FailureUnavailable, RetryAfterMillis: 50},
			want:      &Failure{Category: FailureUnavailable, RetryAfterMillis: 50},
		},
		{
			name:      "begin local compatibility",
			normalize: NormalizeBeginOperationFailure,
			source:    &Failure{Category: FailureIncompatible},
			want: &Failure{
				Category: FailureIncompatible, RequiredCapability: CapabilityName,
				SupportedMajor: ProtocolMajor, SupportedMinorMin: ProtocolMinor, SupportedMinorMax: ProtocolMinor,
			},
		},
		{
			name:      "foreign compatibility fails closed",
			normalize: NormalizeConsumeOperationFailure,
			source: &Failure{
				Category: FailureIncompatible, RequiredCapability: "runtime.identity.exchange.v1",
				SupportedMajor: ProtocolMajor, SupportedMinorMin: ProtocolMinor, SupportedMinorMax: ProtocolMinor,
			},
			want: &Failure{Category: FailureInternal},
		},
		{
			name:      "begin missing fails closed",
			normalize: NormalizeBeginOperationFailure,
			source:    &Failure{Category: FailureMissing},
			want:      &Failure{Category: FailureInternal},
		},
		{
			name:      "begin request failure is not an owner result",
			normalize: NormalizeBeginOperationFailure,
			source:    &Failure{Category: FailureInvalidRequest, InvalidComponent: InvalidComponentFieldValue},
			want:      &Failure{Category: FailureInternal},
		},
		{
			name:      "unreviewed consume category fails closed",
			normalize: NormalizeConsumeOperationFailure,
			source:    &Failure{Category: FailureCategory("FUTURE")},
			want:      &Failure{Category: FailureInternal},
		},
		{
			name:      "unreviewed consume limit fails closed",
			normalize: NormalizeConsumeOperationFailure,
			source:    &Failure{Category: FailureResourceExhausted, LimitKind: LimitAppInstallationID, Maximum: MaxIdentifierBytes},
			want:      &Failure{Category: FailureInternal},
		},
		{
			name:      "opaque owner error fails closed",
			normalize: NormalizeConsumeOperationFailure,
			source:    errors.New("storage detail"),
			want:      &Failure{Category: FailureInternal},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			var failure *Failure
			if err := test.normalize(test.source); !errors.As(err, &failure) || !reflect.DeepEqual(failure, test.want) {
				t.Fatalf("normalized failure = %#v, %v; want %#v", failure, err, test.want)
			}
		})
	}

	if err := NormalizeBeginOperationFailure(nil); err != nil {
		t.Fatalf("nil begin failure = %v", err)
	}
	if err := NormalizeConsumeOperationFailure(nil); err != nil {
		t.Fatalf("nil consume failure = %v", err)
	}
}

var structurallyValidButDisclosingReceipts = common.Receipts{
	ProtocolVersion:   "1.0",
	CatalogRevision:   1,
	SchemaGeneration:  1,
	EngineGeneration:  1,
	SecurityEpoch:     1,
	CapabilityVersion: CapabilityName,
}
