package credentialstore

import (
	"context"
	"path/filepath"
	"testing"

	"github.com/stretchr/testify/require"

	"iop/packages/go/config"
)

func setupRouteTestFixture(t *testing.T) (*Store, *IssuedPrincipal, *CredentialSlotRecord, *CredentialSlotRecord) {
	t.Helper()
	dbPath := filepath.Join(t.TempDir(), "test_routes.db")
	ctx := context.Background()

	reg := newFakeKeyRegistry()
	reg.RegisterKey("k1", 1)

	store, err := Open(ctx, dbPath, WithEnvelopeKeyRegistry(reg))
	require.NoError(t, err)
	t.Cleanup(func() { _ = store.Close() })

	p, err := store.CreatePrincipalWithToken(ctx, CreatePrincipalInput{Alias: "route-principal"})
	require.NoError(t, err)

	env := SecretEnvelope{
		Algorithm:  "AES-256-GCM",
		KeyID:      "k1",
		KeyVersion: 1,
		Nonce:      []byte("123456789012"),
		Ciphertext: []byte("cipher1"),
	}

	openaiSlot, err := store.CreateSlot(ctx, CreateSlotInput{
		PrincipalID:    p.Principal.ID,
		Vendor:         "openai",
		CredentialKind: CredentialKindBearer,
		Alias:          "openai-slot-1",
		Envelope:       env,
	})
	require.NoError(t, err)

	anthropicSlot, err := store.CreateSlot(ctx, CreateSlotInput{
		PrincipalID:    p.Principal.ID,
		Vendor:         "anthropic",
		CredentialKind: CredentialKindAPIKey,
		Alias:          "anthropic-slot-1",
		Envelope:       env,
	})
	require.NoError(t, err)

	return store, p, openaiSlot, anthropicSlot
}

func TestRoutesDisambiguateSameUpstreamModelBySlot(t *testing.T) {
	reg := newFakeKeyRegistry()
	reg.RegisterKey("k1", 1)
	dbPath := filepath.Join(t.TempDir(), "disambig.db")
	ctx := context.Background()

	store, err := Open(ctx, dbPath, WithEnvelopeKeyRegistry(reg))
	require.NoError(t, err)
	defer store.Close()

	p, err := store.CreatePrincipalWithToken(ctx, CreatePrincipalInput{Alias: "p-disambig"})
	require.NoError(t, err)

	env := SecretEnvelope{
		Algorithm:  "AES-256-GCM",
		KeyID:      "k1",
		KeyVersion: 1,
		Nonce:      []byte("123456789012"),
		Ciphertext: []byte("cipher1"),
	}

	slotA, err := store.CreateSlot(ctx, CreateSlotInput{
		PrincipalID:    p.Principal.ID,
		Vendor:         "openai",
		CredentialKind: CredentialKindBearer,
		Alias:          "slot-a",
		Envelope:       env,
	})
	require.NoError(t, err)

	slotB, err := store.CreateSlot(ctx, CreateSlotInput{
		PrincipalID:    p.Principal.ID,
		Vendor:         "openai",
		CredentialKind: CredentialKindBearer,
		Alias:          "slot-b",
		Envelope:       env,
	})
	require.NoError(t, err)

	// Create Route 1 referencing slotA for gpt-4o
	route1, err := store.CreateRoute(ctx, CreateRouteInput{
		PrincipalID:   p.Principal.ID,
		SlotID:        slotA.ID,
		Alias:         "route-gpt4o-a",
		ProfileID:     "openai",
		UpstreamModel: "gpt-4o",
	})
	require.NoError(t, err)
	require.Equal(t, slotA.ID, route1.SlotID)

	// Create Route 2 referencing slotB for gpt-4o
	route2, err := store.CreateRoute(ctx, CreateRouteInput{
		PrincipalID:   p.Principal.ID,
		SlotID:        slotB.ID,
		Alias:         "route-gpt4o-b",
		ProfileID:     "openai",
		UpstreamModel: "gpt-4o",
	})
	require.NoError(t, err)
	require.Equal(t, slotB.ID, route2.SlotID)

	routes, err := store.ListRoutes(ctx, p.Principal.ID)
	require.NoError(t, err)
	require.Len(t, routes, 2)
}

func TestSlotSupportsMultipleCompatibleRoutes(t *testing.T) {
	store, p, openaiSlot, _ := setupRouteTestFixture(t)
	ctx := context.Background()

	r1, err := store.CreateRoute(ctx, CreateRouteInput{
		PrincipalID:   p.Principal.ID,
		SlotID:        openaiSlot.ID,
		Alias:         "route-gpt4o",
		ProfileID:     "openai",
		UpstreamModel: "gpt-4o",
	})
	require.NoError(t, err)

	r2, err := store.CreateRoute(ctx, CreateRouteInput{
		PrincipalID:   p.Principal.ID,
		SlotID:        openaiSlot.ID,
		Alias:         "route-gpt4o-mini",
		ProfileID:     "openai",
		UpstreamModel: "gpt-4o-mini",
	})
	require.NoError(t, err)

	slotRoutes, err := store.ListRoutesBySlot(ctx, p.Principal.ID, openaiSlot.ID)
	require.NoError(t, err)
	require.Len(t, slotRoutes, 2)
	require.Equal(t, r1.ID, slotRoutes[0].ID)
	require.Equal(t, r2.ID, slotRoutes[1].ID)
}

func TestRouteRejectsCrossPrincipalSlot(t *testing.T) {
	store, _, openaiSlotP1, _ := setupRouteTestFixture(t)
	ctx := context.Background()

	p2, err := store.CreatePrincipalWithToken(ctx, CreatePrincipalInput{Alias: "principal-2"})
	require.NoError(t, err)

	// Attempting to create a route for P2 using P1's slot must fail
	_, err = store.CreateRoute(ctx, CreateRouteInput{
		PrincipalID:   p2.Principal.ID,
		SlotID:        openaiSlotP1.ID,
		Alias:         "p2-route",
		ProfileID:     "openai",
		UpstreamModel: "gpt-4o",
	})
	require.Error(t, err)
	require.ErrorIs(t, err, ErrCrossPrincipalSlot)
}

func TestRouteRejectsIncompatibleProfile(t *testing.T) {
	store, p, openaiSlot, anthropicSlot := setupRouteTestFixture(t)
	ctx := context.Background()

	// OpenAI slot with Anthropic profile should fail
	_, err := store.CreateRoute(ctx, CreateRouteInput{
		PrincipalID:   p.Principal.ID,
		SlotID:        openaiSlot.ID,
		Alias:         "bad-route-1",
		ProfileID:     "anthropic",
		UpstreamModel: "claude-3-5-sonnet",
	})
	require.Error(t, err)
	require.ErrorIs(t, err, ErrIncompatibleProfile)

	// Anthropic slot with OpenAI profile should fail
	_, err = store.CreateRoute(ctx, CreateRouteInput{
		PrincipalID:   p.Principal.ID,
		SlotID:        anthropicSlot.ID,
		Alias:         "bad-route-2",
		ProfileID:     "openai",
		UpstreamModel: "gpt-4o",
	})
	require.Error(t, err)
	require.ErrorIs(t, err, ErrIncompatibleProfile)
}

func TestRouteAliasCollisionFailsClosed(t *testing.T) {
	store, p, openaiSlot, _ := setupRouteTestFixture(t)
	ctx := context.Background()

	_, err := store.CreateRoute(ctx, CreateRouteInput{
		PrincipalID:   p.Principal.ID,
		SlotID:        openaiSlot.ID,
		Alias:         "my-route-alias",
		ProfileID:     "openai",
		UpstreamModel: "gpt-4o",
	})
	require.NoError(t, err)

	_, err = store.CreateRoute(ctx, CreateRouteInput{
		PrincipalID:   p.Principal.ID,
		SlotID:        openaiSlot.ID,
		Alias:         "my-route-alias",
		ProfileID:     "openai",
		UpstreamModel: "gpt-4o-mini",
	})
	require.Error(t, err)
	require.ErrorIs(t, err, ErrRouteAliasAlreadyExists)
}

func TestSlotSupportsCompatibleChatAndMessagesProfiles(t *testing.T) {
	reg := newFakeKeyRegistry()
	reg.RegisterKey("k1", 1)
	store, p := setupTestStoreWithRegistry(t, reg)
	ctx := context.Background()
	slot, err := store.CreateSlot(ctx, CreateSlotInput{
		PrincipalID: p.Principal.ID, Vendor: "minimax", CredentialKind: CredentialKindBearer, Alias: "minimax",
		Envelope: SecretEnvelope{Algorithm: "AES-256-GCM", KeyID: "k1", KeyVersion: 1, Nonce: []byte("123456789012"), Ciphertext: []byte("opaque")},
	})
	require.NoError(t, err)
	chat, err := store.CreateRoute(ctx, CreateRouteInput{PrincipalID: p.Principal.ID, SlotID: slot.ID, Alias: "mini-chat", ProfileID: "minimax_chat", UpstreamModel: "MiniMax-M2"})
	require.NoError(t, err)
	messages, err := store.CreateRoute(ctx, CreateRouteInput{PrincipalID: p.Principal.ID, SlotID: slot.ID, Alias: "mini-messages", ProfileID: "minimax_messages", UpstreamModel: "MiniMax-M2"})
	require.NoError(t, err)
	require.NotEqual(t, chat.ProfileID, messages.ProfileID)
	routes, err := store.ListRoutesBySlot(ctx, p.Principal.ID, slot.ID)
	require.NoError(t, err)
	require.Len(t, routes, 2)
}

// TestGLMSlotSupportsGeneralAndCodingProfiles verifies that a single
// glm/bearer credential slot can bind two distinct routes — one for General
// API and one for Coding Plan — each with its own alias, profile, and
// resource selector, while a different vendor slot rejects glm_coding.
// Official endpoint distinction rechecked on 2026-08-02:
// https://docs.z.ai/api-reference/introduction
// https://docs.z.ai/scenario-example/develop-tools/others
func TestGLMSlotSupportsGeneralAndCodingProfiles(t *testing.T) {
	reg := newFakeKeyRegistry()
	reg.RegisterKey("k1", 1)
	store, p := setupTestStoreWithRegistry(t, reg)
	ctx := context.Background()

	env := SecretEnvelope{
		Algorithm:  "AES-256-GCM",
		KeyID:      "k1",
		KeyVersion: 1,
		Nonce:      []byte("123456789012"),
		Ciphertext: []byte("opaque-glm"),
	}

	slot, err := store.CreateSlot(ctx, CreateSlotInput{
		PrincipalID:    p.Principal.ID,
		Vendor:         "glm",
		CredentialKind: CredentialKindBearer,
		Alias:          "glm-slot",
		Envelope:       env,
	})
	require.NoError(t, err)

	apiRoute, err := store.CreateRoute(ctx, CreateRouteInput{
		PrincipalID:      p.Principal.ID,
		SlotID:           slot.ID,
		Alias:            "glm-5.1-api",
		ProfileID:        "glm",
		UpstreamModel:    "glm-5.1",
		ResourceSelector: "glm-api",
	})
	require.NoError(t, err)
	require.Equal(t, "glm", apiRoute.ProfileID)
	require.Equal(t, "glm-5.1-api", apiRoute.Alias)
	require.Equal(t, "glm-api", apiRoute.ResourceSelector)

	codingRoute, err := store.CreateRoute(ctx, CreateRouteInput{
		PrincipalID:      p.Principal.ID,
		SlotID:           slot.ID,
		Alias:            "glm-5.1-coding",
		ProfileID:        "glm_coding",
		UpstreamModel:    "glm-5.1",
		ResourceSelector: "glm-coding",
	})
	require.NoError(t, err)
	require.Equal(t, "glm_coding", codingRoute.ProfileID)
	require.Equal(t, "glm-5.1-coding", codingRoute.Alias)
	require.Equal(t, "glm-coding", codingRoute.ResourceSelector)

	require.NotEqual(t, apiRoute.ID, codingRoute.ID, "two routes must have distinct IDs")

	routes, err := store.ListRoutes(ctx, p.Principal.ID)
	require.NoError(t, err)
	require.Len(t, routes, 2)

	routesBySlot, err := store.ListRoutesBySlot(ctx, p.Principal.ID, slot.ID)
	require.NoError(t, err)
	require.Len(t, routesBySlot, 2)

	// A different-vendor slot must reject glm_coding.
	otherSlot, err := store.CreateSlot(ctx, CreateSlotInput{
		PrincipalID:    p.Principal.ID,
		Vendor:         "openai",
		CredentialKind: CredentialKindBearer,
		Alias:          "openai-slot",
		Envelope:       env,
	})
	require.NoError(t, err)
	_, err = store.CreateRoute(ctx, CreateRouteInput{
		PrincipalID:   p.Principal.ID,
		SlotID:        otherSlot.ID,
		Alias:         "openai-glm-coding",
		ProfileID:     "glm_coding",
		UpstreamModel: "glm-5.1",
	})
	require.ErrorIs(t, err, ErrIncompatibleProfile)
}

func TestRouteRejectsUnknownVendorCredentialTuple(t *testing.T) {
	reg := newFakeKeyRegistry()
	reg.RegisterKey("k1", 1)
	store, p := setupTestStoreWithRegistry(t, reg)
	slot, err := store.CreateSlot(context.Background(), CreateSlotInput{
		PrincipalID: p.Principal.ID, Vendor: "unknown-provider", CredentialKind: CredentialKindBearer, Alias: "unknown",
		Envelope: SecretEnvelope{Algorithm: "AES-256-GCM", KeyID: "k1", KeyVersion: 1, Nonce: []byte("123456789012"), Ciphertext: []byte("opaque")},
	})
	require.NoError(t, err)
	_, err = store.CreateRoute(context.Background(), CreateRouteInput{PrincipalID: p.Principal.ID, SlotID: slot.ID, ProfileID: "openai", UpstreamModel: "model"})
	require.ErrorIs(t, err, ErrIncompatibleProfile)

	mismatched, err := store.CreateSlot(context.Background(), CreateSlotInput{
		PrincipalID: p.Principal.ID, Vendor: "openai", CredentialKind: CredentialKindAPIKey, Alias: "mismatched",
		Envelope: SecretEnvelope{Algorithm: "AES-256-GCM", KeyID: "k1", KeyVersion: 1, Nonce: []byte("123456789012"), Ciphertext: []byte("opaque-2")},
	})
	require.NoError(t, err)
	_, err = store.CreateRoute(context.Background(), CreateRouteInput{PrincipalID: p.Principal.ID, SlotID: mismatched.ID, ProfileID: "openai", UpstreamModel: "model"})
	require.ErrorIs(t, err, ErrIncompatibleProfile)
}

func TestRouteResolutionUsesImmutableProfileCatalog(t *testing.T) {
	store, p, slot, _ := setupRouteTestFixture(t)
	ctx := context.Background()

	original, ok := config.BuiltInProtocolProfiles["openai"]
	require.True(t, ok)
	delete(config.BuiltInProtocolProfiles, "openai")
	t.Cleanup(func() { config.BuiltInProtocolProfiles["openai"] = original })

	route, err := store.CreateRoute(ctx, CreateRouteInput{
		PrincipalID:   p.Principal.ID,
		SlotID:        slot.ID,
		ProfileID:     "openai",
		UpstreamModel: "gpt-4o",
	})
	require.NoError(t, err)
	require.Equal(t, "openai", route.ProfileID)
}

func TestRouteRejectsInactiveSlot(t *testing.T) {
	store, p, slot, _ := setupRouteTestFixture(t)
	ctx := context.Background()
	first, err := store.CreateRoute(ctx, CreateRouteInput{PrincipalID: p.Principal.ID, SlotID: slot.ID, ProfileID: "openai", UpstreamModel: "gpt-4o"})
	require.NoError(t, err)
	loaded, err := store.GetSlot(ctx, p.Principal.ID, slot.ID)
	require.NoError(t, err)
	disabled, err := store.DisableSlot(ctx, p.Principal.ID, slot.ID, loaded.Revision)
	require.NoError(t, err)
	_, err = store.CreateRoute(ctx, CreateRouteInput{PrincipalID: p.Principal.ID, SlotID: slot.ID, ProfileID: "openai", UpstreamModel: "gpt-4o-mini"})
	require.ErrorIs(t, err, ErrSlotNotActive)
	revoked, err := store.RevokeSlot(ctx, p.Principal.ID, slot.ID, disabled.Revision)
	require.NoError(t, err)
	_, err = store.CreateRoute(ctx, CreateRouteInput{PrincipalID: p.Principal.ID, SlotID: slot.ID, ProfileID: "openai", UpstreamModel: "gpt-4.1"})
	require.ErrorIs(t, err, ErrSlotRevoked)
	require.NotEmpty(t, first.ID)
	require.Equal(t, StatusRevoked, revoked.Status)
}

func TestEnableRouteRejectsInactiveSlot(t *testing.T) {
	store, p, slot, _ := setupRouteTestFixture(t)
	ctx := context.Background()
	route, err := store.CreateRoute(ctx, CreateRouteInput{PrincipalID: p.Principal.ID, SlotID: slot.ID, ProfileID: "openai", UpstreamModel: "gpt-4o"})
	require.NoError(t, err)
	disabledRoute, err := store.DisableRoute(ctx, p.Principal.ID, route.ID, route.Revision)
	require.NoError(t, err)
	loaded, err := store.GetSlot(ctx, p.Principal.ID, slot.ID)
	require.NoError(t, err)
	disabledSlot, err := store.DisableSlot(ctx, p.Principal.ID, slot.ID, loaded.Revision)
	require.NoError(t, err)
	_, err = store.EnableRoute(ctx, p.Principal.ID, route.ID, disabledRoute.Revision)
	require.ErrorIs(t, err, ErrSlotNotActive)
	_, err = store.EnableSlot(ctx, p.Principal.ID, slot.ID, disabledSlot.Revision)
	require.NoError(t, err)
	enabledRoute, err := store.EnableRoute(ctx, p.Principal.ID, route.ID, disabledRoute.Revision)
	require.NoError(t, err)
	require.Equal(t, StatusActive, enabledRoute.Status)
}

func TestUpdateRouteUsesRevisionCAS(t *testing.T) {
	store, p, slot, _ := setupRouteTestFixture(t)
	ctx := context.Background()
	route, err := store.CreateRoute(ctx, CreateRouteInput{PrincipalID: p.Principal.ID, SlotID: slot.ID, Alias: "before", ProfileID: "openai", UpstreamModel: "gpt-4o"})
	require.NoError(t, err)
	updated, err := store.UpdateRoute(ctx, UpdateRouteInput{PrincipalID: p.Principal.ID, RouteID: route.ID, CurrentRevision: route.Revision, SlotID: slot.ID, Alias: " after ", ProfileID: "openai", UpstreamModel: "gpt-4.1", ResourceSelector: "preferred"})
	require.NoError(t, err)
	require.Equal(t, int64(1), updated.Revision)
	require.Equal(t, "after", updated.Alias)
	require.Equal(t, "gpt-4.1", updated.UpstreamModel)
	require.Equal(t, "preferred", updated.ResourceSelector)
	_, err = store.UpdateRoute(ctx, UpdateRouteInput{PrincipalID: p.Principal.ID, RouteID: route.ID, CurrentRevision: route.Revision, SlotID: slot.ID, Alias: "stale", ProfileID: "openai", UpstreamModel: "gpt-4o"})
	require.ErrorIs(t, err, ErrRevisionMismatch)
}

func TestRouteAliasCannotShadowRouteID(t *testing.T) {
	store, p, slot, _ := setupRouteTestFixture(t)
	first, err := store.CreateRoute(context.Background(), CreateRouteInput{PrincipalID: p.Principal.ID, SlotID: slot.ID, Alias: "first", ProfileID: "openai", UpstreamModel: "gpt-4o"})
	require.NoError(t, err)
	_, err = store.CreateRoute(context.Background(), CreateRouteInput{PrincipalID: p.Principal.ID, SlotID: slot.ID, Alias: first.ID, ProfileID: "openai", UpstreamModel: "gpt-4.1"})
	require.ErrorIs(t, err, ErrRouteAliasAlreadyExists)
}

func TestRouteLifecyclePersistsAcrossReopen(t *testing.T) {
	ctx := context.Background()
	dbPath := filepath.Join(t.TempDir(), "route-lifecycle.db")
	reg := newFakeKeyRegistry()
	reg.RegisterKey("k1", 1)
	store, err := Open(ctx, dbPath, WithEnvelopeKeyRegistry(reg))
	require.NoError(t, err)
	p, err := store.CreatePrincipalWithToken(ctx, CreatePrincipalInput{Alias: "route-reopen"})
	require.NoError(t, err)
	slot, err := store.CreateSlot(ctx, CreateSlotInput{
		PrincipalID: p.Principal.ID, Vendor: "openai", CredentialKind: CredentialKindBearer, Alias: "reopen-slot",
		Envelope: SecretEnvelope{Algorithm: "AES-256-GCM", KeyID: "k1", KeyVersion: 1, Nonce: []byte("123456789012"), Ciphertext: []byte("opaque")},
	})
	require.NoError(t, err)
	route, err := store.CreateRoute(ctx, CreateRouteInput{PrincipalID: p.Principal.ID, SlotID: slot.ID, Alias: "reopen-route", ProfileID: "openai", UpstreamModel: "gpt-4o", ResourceSelector: "primary"})
	require.NoError(t, err)
	disabled, err := store.DisableRoute(ctx, p.Principal.ID, route.ID, route.Revision)
	require.NoError(t, err)
	require.NoError(t, store.Close())

	reopened, err := Open(ctx, dbPath, WithEnvelopeKeyRegistry(reg))
	require.NoError(t, err)
	defer reopened.Close()
	loaded, err := reopened.GetRoute(ctx, p.Principal.ID, route.ID)
	require.NoError(t, err)
	require.Equal(t, StatusDisabled, loaded.Status)
	require.Equal(t, disabled.Revision, loaded.Revision)
	require.Equal(t, "reopen-route", loaded.Alias)
	require.Equal(t, "primary", loaded.ResourceSelector)
}
