package runtimepermissionstate

import (
	"context"
	"errors"
	"reflect"
	"runtime/debug"
	"slices"
	"testing"
	"time"

	"github.com/nexuscrm/backend/internal/runtimeconfig"
	"github.com/nexuscrm/persistence"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/permissionstatev1"
)

func TestGateCurrentServingSchemasAdvertiseExactPermissionStateCapability(t *testing.T) {
	for _, schema := range []string{
		runtimeconfig.ServingConfigSchemaV3,
		runtimeconfig.ServingConfigSchemaV4,
	} {
		t.Run(schema, func(t *testing.T) {
			lineage := testLineage(t, 1)
			source := &stateSourceStub{states: []persistence.PermissionState{
				testState(t, lineage, 7, persistence.PermissionStateDarkMode),
			}}
			gate, err := newGate(context.Background(), source, Config{
				SourceRevision:          "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
				ConfigurationSchema:     schema,
				PermissionStateProtocol: ProtocolV1,
				Role:                    RoleReaderWriter,
			}, capabilityFacts{
				schemaFingerprint:    "sha256:schema",
				mutationDigest:       "sha256:mutations",
				directWriterDigest:   "sha256:writers",
				authorizationCompose: "nexuscrm.permission-state-v1-authorization.v1",
			})
			if err != nil {
				t.Fatal(err)
			}
			want := []string{
				"permission_state.composition=nexuscrm.permission-state-v1-authorization.v1",
				"permission_state.config=" + schema,
				"permission_state.direct_writer_inventory=sha256:writers",
				"permission_state.lineage=01010101010101010101010101010101",
				"permission_state.mode_support=dark,enforced",
				"permission_state.mutation_inventory=sha256:mutations",
				"permission_state.role=reader-writer",
				"permission_state.schema=sha256:schema",
				"permission_state.source_revision=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
				ProtocolV1,
			}
			if got := gate.Capabilities(); !reflect.DeepEqual(got, want) {
				t.Fatalf("Capabilities() = %#v, want %#v", got, want)
			}
			got := gate.Capabilities()
			got[0] = "mutated"
			if reflect.DeepEqual(got, gate.Capabilities()) {
				t.Fatal("Capabilities returned mutable module-owned state")
			}
		})
	}
}

func TestGateRejectsLegacyConfigAgainstEnforcedState(t *testing.T) {
	lineage := testLineage(t, 2)
	_, err := newGate(
		context.Background(),
		&stateSourceStub{states: []persistence.PermissionState{
			testState(t, lineage, 8, persistence.PermissionStateEnforcedMode),
		}},
		Config{
			SourceRevision:      "dev",
			ConfigurationSchema: "nexuscrm.runtime-api-serving/v2",
			Role:                RoleReader,
		},
		capabilityFacts{},
	)
	if err == nil {
		t.Fatal("legacy v2 configuration started against enforced state")
	}
}

func TestGateWorkerSchemasFenceLegacyAndAdvertiseV3(t *testing.T) {
	lineage := testLineage(t, 6)
	for _, schema := range []string{
		runtimeconfig.WorkerConfigSchemaV1,
		runtimeconfig.WorkerConfigSchemaV2,
	} {
		t.Run(schema, func(t *testing.T) {
			_, err := newGate(
				context.Background(),
				&stateSourceStub{states: []persistence.PermissionState{
					testState(t, lineage, 8, persistence.PermissionStateEnforcedMode),
				}},
				Config{
					SourceRevision:      "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
					ConfigurationSchema: schema,
					Role:                RoleReaderWriter,
				},
				capabilityFacts{},
			)
			if err == nil {
				t.Fatal("legacy worker schema started against enforced state")
			}

			legacy, err := newGate(
				context.Background(),
				&stateSourceStub{states: []persistence.PermissionState{
					testState(t, lineage, 7, persistence.PermissionStateDarkMode),
					testState(t, lineage, 8, persistence.PermissionStateEnforcedMode),
				}},
				Config{
					SourceRevision:      "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
					ConfigurationSchema: schema,
					Role:                RoleReaderWriter,
				},
				capabilityFacts{},
			)
			if err != nil {
				t.Fatal(err)
			}
			if len(legacy.Capabilities()) != 0 {
				t.Fatalf("legacy worker capabilities = %v, want empty", legacy.Capabilities())
			}
			if legacy.Ready() {
				t.Fatal("legacy worker remained ready after enforcement")
			}
		})
	}

	gate, err := newGate(
		context.Background(),
		&stateSourceStub{states: []persistence.PermissionState{
			testState(t, lineage, 8, persistence.PermissionStateEnforcedMode),
		}},
		Config{
			SourceRevision:          "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
			ConfigurationSchema:     runtimeconfig.WorkerConfigSchemaV3,
			PermissionStateProtocol: ProtocolV1,
			Role:                    RoleReaderWriter,
		},
		validCapabilityFacts(),
	)
	if err != nil {
		t.Fatal(err)
	}
	capabilities := gate.Capabilities()
	for _, want := range []string{
		ProtocolV1,
		"permission_state.config=" + runtimeconfig.WorkerConfigSchemaV3,
		"permission_state.role=reader-writer",
	} {
		if !slices.Contains(capabilities, want) {
			t.Fatalf("worker capabilities %v missing %q", capabilities, want)
		}
	}
}

func TestGateServerSchemasFenceLegacyAndAdvertiseV1(t *testing.T) {
	lineage := testLineage(t, 7)
	if _, err := newGate(
		context.Background(),
		&stateSourceStub{states: []persistence.PermissionState{
			testState(t, lineage, 8, persistence.PermissionStateEnforcedMode),
		}},
		Config{
			SourceRevision:      "dev",
			ConfigurationSchema: runtimeconfig.ServerPermissionStateLegacySchema,
			Role:                RoleReaderWriter,
		},
		capabilityFacts{},
	); err == nil {
		t.Fatal("legacy server started against enforced state")
	}

	legacy, err := newGate(
		context.Background(),
		&stateSourceStub{states: []persistence.PermissionState{
			testState(t, lineage, 7, persistence.PermissionStateDarkMode),
			testState(t, lineage, 8, persistence.PermissionStateEnforcedMode),
		}},
		Config{
			SourceRevision:      "dev",
			ConfigurationSchema: runtimeconfig.ServerPermissionStateLegacySchema,
			Role:                RoleReaderWriter,
		},
		capabilityFacts{},
	)
	if err != nil {
		t.Fatal(err)
	}
	if len(legacy.Capabilities()) != 0 || legacy.Ready() {
		t.Fatalf(
			"legacy server capabilities/readiness = %v/%t, want empty/false",
			legacy.Capabilities(),
			legacy.Ready(),
		)
	}

	current, err := newGate(
		context.Background(),
		&stateSourceStub{states: []persistence.PermissionState{
			testState(t, lineage, 8, persistence.PermissionStateEnforcedMode),
		}},
		Config{
			SourceRevision:          "dev",
			ConfigurationSchema:     runtimeconfig.ServerPermissionStateConfigSchemaV1,
			PermissionStateProtocol: ProtocolV1,
			Role:                    RoleReaderWriter,
		},
		validCapabilityFacts(),
	)
	if err != nil {
		t.Fatal(err)
	}
	for _, want := range []string{
		ProtocolV1,
		"permission_state.config=" + runtimeconfig.ServerPermissionStateConfigSchemaV1,
		"permission_state.role=reader-writer",
	} {
		if !slices.Contains(current.Capabilities(), want) {
			t.Fatalf("server capabilities %v missing %q", current.Capabilities(), want)
		}
	}
}

func TestCapabilityFactsDoNotDependOnBuildModuleVersions(t *testing.T) {
	for _, info := range []*debug.BuildInfo{
		nil,
		{Deps: []*debug.Module{{
			Path: "github.com/pingcap-inc/nexuscrm-contract", Version: "(devel)",
		}}},
	} {
		facts, err := loadCapabilityFacts(info)
		if err != nil {
			t.Fatalf("loadCapabilityFacts(%#v): %v", info, err)
		}
		if err := facts.validate(); err != nil {
			t.Fatalf("capability facts from workspace build are invalid: %v", err)
		}
	}
}

func TestGateV3RejectsIncompleteCapabilityIdentity(t *testing.T) {
	validConfig := Config{
		SourceRevision:          "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
		ConfigurationSchema:     "nexuscrm.runtime-api-serving/v3",
		PermissionStateProtocol: ProtocolV1,
		Role:                    RoleReader,
	}
	validFacts := capabilityFacts{
		schemaFingerprint:    "sha256:schema",
		mutationDigest:       "sha256:mutations",
		directWriterDigest:   "sha256:writers",
		authorizationCompose: "nexuscrm.permission-state-v1-authorization.v1",
	}
	for _, test := range []struct {
		name   string
		mutate func(*Config, *capabilityFacts)
	}{
		{name: "nonexact source", mutate: func(config *Config, _ *capabilityFacts) { config.SourceRevision = "main" }},
		{name: "unknown protocol", mutate: func(config *Config, _ *capabilityFacts) { config.PermissionStateProtocol = "permission_state.v2" }},
		{name: "unknown role", mutate: func(config *Config, _ *capabilityFacts) { config.Role = "writer" }},
		{name: "missing schema", mutate: func(_ *Config, facts *capabilityFacts) { facts.schemaFingerprint = "" }},
		{name: "missing mutation inventory", mutate: func(_ *Config, facts *capabilityFacts) { facts.mutationDigest = "" }},
		{name: "missing direct writers", mutate: func(_ *Config, facts *capabilityFacts) { facts.directWriterDigest = "" }},
		{name: "missing composition", mutate: func(_ *Config, facts *capabilityFacts) { facts.authorizationCompose = "" }},
	} {
		t.Run(test.name, func(t *testing.T) {
			config, facts := validConfig, validFacts
			test.mutate(&config, &facts)
			_, err := newGate(
				context.Background(),
				&stateSourceStub{states: []persistence.PermissionState{
					testState(t, testLineage(t, 3), 1, persistence.PermissionStateDarkMode),
				}},
				config,
				facts,
			)
			if err == nil {
				t.Fatal("incomplete v3 capability identity was accepted")
			}
		})
	}
}

func TestGateReadinessReprovesLineageAndMode(t *testing.T) {
	lineage := testLineage(t, 4)
	otherLineage := testLineage(t, 5)
	tests := []struct {
		name        string
		schema      string
		protocol    string
		next        persistence.PermissionState
		nextErr     error
		wantReady   bool
		initialMode persistence.PermissionStateEnforcementMode
	}{
		{
			name:   "v3 remains ready after enforcement",
			schema: "nexuscrm.runtime-api-serving/v3", protocol: ProtocolV1,
			next:      testState(t, lineage, 8, persistence.PermissionStateEnforcedMode),
			wantReady: true,
		},
		{
			name:   "v2 leaves readiness after enforcement",
			schema: "nexuscrm.runtime-api-serving/v2",
			next:   testState(t, lineage, 8, persistence.PermissionStateEnforcedMode),
		},
		{
			name:   "lineage replacement leaves readiness",
			schema: "nexuscrm.runtime-api-serving/v3", protocol: ProtocolV1,
			next: testState(t, otherLineage, 1, persistence.PermissionStateDarkMode),
		},
		{
			name:   "authority outage leaves readiness",
			schema: "nexuscrm.runtime-api-serving/v3", protocol: ProtocolV1,
			nextErr: errors.New("database unavailable"),
		},
		{
			name:   "same-lineage revision regression leaves readiness",
			schema: "nexuscrm.runtime-api-serving/v3", protocol: ProtocolV1,
			next: testState(t, lineage, 6, persistence.PermissionStateDarkMode),
		},
		{
			name:        "enforced downgrade leaves readiness",
			schema:      "nexuscrm.runtime-api-serving/v3",
			protocol:    ProtocolV1,
			initialMode: persistence.PermissionStateEnforcedMode,
			next:        testState(t, lineage, 8, persistence.PermissionStateDarkMode),
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			initialMode := test.initialMode
			if initialMode == "" {
				initialMode = persistence.PermissionStateDarkMode
			}
			source := &stateSourceStub{
				states: []persistence.PermissionState{
					testState(t, lineage, 7, initialMode),
					test.next,
				},
				errors: []error{nil, test.nextErr},
			}
			gate, err := newGate(context.Background(), source, Config{
				SourceRevision:          "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
				ConfigurationSchema:     test.schema,
				PermissionStateProtocol: test.protocol,
				Role:                    RoleReader,
				ReadinessTimeout:        time.Second,
			}, validCapabilityFacts())
			if err != nil {
				t.Fatal(err)
			}
			if got := gate.Ready(); got != test.wantReady {
				t.Fatalf("Ready() = %t, want %t", got, test.wantReady)
			}
		})
	}
}

func TestGateRejectsUnavailableInputs(t *testing.T) {
	if _, err := newGate(context.Background(), nil, Config{}, capabilityFacts{}); err == nil {
		t.Fatal("newGate(nil source) returned nil error")
	}
	if _, err := newGate(nil, &stateSourceStub{}, Config{}, capabilityFacts{}); err == nil {
		t.Fatal("newGate(nil context) returned nil error")
	}
	var unavailable *Gate
	if unavailable.Ready() {
		t.Fatal("nil Gate reported ready")
	}
}

type stateSourceStub struct {
	states []persistence.PermissionState
	errors []error
	calls  int
}

func (source *stateSourceStub) CheckReady(context.Context) (persistence.PermissionState, error) {
	return source.next()
}

func (source *stateSourceStub) LoadCurrent(context.Context) (persistence.PermissionState, error) {
	return source.next()
}

func (source *stateSourceStub) next() (persistence.PermissionState, error) {
	index := source.calls
	source.calls++
	var state persistence.PermissionState
	if index < len(source.states) {
		state = source.states[index]
	}
	var err error
	if index < len(source.errors) {
		err = source.errors[index]
	}
	return state, err
}

func testLineage(t *testing.T, value byte) permissionstatev1.Lineage {
	t.Helper()
	lineage, err := permissionstatev1.LineageFromBytes(
		[]byte{value, value, value, value, value, value, value, value, value, value, value, value, value, value, value, value},
	)
	if err != nil {
		t.Fatal(err)
	}
	return lineage
}

func testState(
	t *testing.T,
	lineage permissionstatev1.Lineage,
	revision uint64,
	mode persistence.PermissionStateEnforcementMode,
) persistence.PermissionState {
	t.Helper()
	version, err := permissionstatev1.NewVersion(lineage, revision)
	if err != nil {
		t.Fatal(err)
	}
	return persistence.PermissionState{Version: version, EnforcementMode: mode}
}

func validCapabilityFacts() capabilityFacts {
	return capabilityFacts{
		schemaFingerprint:    "sha256:schema",
		mutationDigest:       "sha256:mutations",
		directWriterDigest:   "sha256:writers",
		authorizationCompose: "nexuscrm.permission-state-v1-authorization.v1",
	}
}
