package credentialseal

import (
	"bytes"
	"context"
	"encoding/base64"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"

	"iop/apps/control-plane/internal/credentialstore"
)

// keyMaterial returns base64 of a deterministic 32-byte key filled with fill.
func keyMaterial(fill byte) string {
	return base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{fill}, aeadKeyLength))
}

// manifestYAML renders a strict manifest for the given entries.
func manifestYAML(entries ...manifestKey) string {
	var sb strings.Builder
	sb.WriteString("keys:\n")
	for _, e := range entries {
		sb.WriteString(fmt.Sprintf("  - id: %q\n    version: %d\n    material: %q\n", e.ID, e.Version, e.Material))
	}
	return sb.String()
}

func newTestKeyring(t *testing.T, active keyRef, entries ...manifestKey) *Keyring {
	t.Helper()
	kr, err := newKeyring([]byte(manifestYAML(entries...)), active, nil)
	require.NoError(t, err)
	return kr
}

func writeFileMode(t *testing.T, path, body string, perm os.FileMode) {
	t.Helper()
	require.NoError(t, os.WriteFile(path, []byte(body), perm))
}

func testContext() Context {
	return Context{PrincipalID: "principal-1", SlotID: "slot-1", Kind: "bearer"}
}

func TestKeyringSealOpenRoundTrip(t *testing.T) {
	kr := newTestKeyring(t, keyRef{"primary", 1}, manifestKey{"primary", 1, keyMaterial(0x11)})
	ctx := context.Background()
	plaintext := []byte("provider-secret-payload")

	env, err := kr.Seal(ctx, plaintext, testContext())
	require.NoError(t, err)
	require.Equal(t, AlgorithmAES256GCM, env.Algorithm)
	require.Equal(t, "primary", env.KeyID)
	require.Equal(t, uint64(1), env.KeyVersion)
	require.Len(t, env.Nonce, 12)
	require.NotEmpty(t, env.Ciphertext)
	require.NotEmpty(t, env.AAD)
	require.NotEqual(t, plaintext, env.Ciphertext)
	require.NotContains(t, string(env.Ciphertext), "provider-secret-payload")

	opened, err := kr.Open(ctx, env, testContext())
	require.NoError(t, err)
	require.Equal(t, plaintext, opened)
}

func TestKeyringNonceUniqueness(t *testing.T) {
	kr := newTestKeyring(t, keyRef{"primary", 1}, manifestKey{"primary", 1, keyMaterial(0x22)})
	ctx := context.Background()
	plaintext := []byte("same-plaintext")

	env1, err := kr.Seal(ctx, plaintext, testContext())
	require.NoError(t, err)
	env2, err := kr.Seal(ctx, plaintext, testContext())
	require.NoError(t, err)

	require.NotEqual(t, env1.Nonce, env2.Nonce)
	require.NotEqual(t, env1.Ciphertext, env2.Ciphertext)
}

func TestKeyringWrongContextFails(t *testing.T) {
	kr := newTestKeyring(t, keyRef{"primary", 1}, manifestKey{"primary", 1, keyMaterial(0x33)})
	ctx := context.Background()
	env, err := kr.Seal(ctx, []byte("secret"), testContext())
	require.NoError(t, err)

	for _, wrong := range []Context{
		{PrincipalID: "other", SlotID: "slot-1", Kind: "bearer"},
		{PrincipalID: "principal-1", SlotID: "other", Kind: "bearer"},
		{PrincipalID: "principal-1", SlotID: "slot-1", Kind: "api_key"},
	} {
		_, err := kr.Open(ctx, env, wrong)
		require.ErrorIs(t, err, ErrOpenFailed)
	}
}

func TestKeyringTamperFails(t *testing.T) {
	kr := newTestKeyring(t, keyRef{"primary", 1}, manifestKey{"primary", 1, keyMaterial(0x44)})
	ctx := context.Background()
	base, err := kr.Seal(ctx, []byte("tamper-target"), testContext())
	require.NoError(t, err)

	tamperCiphertext := clone(base)
	tamperCiphertext.Ciphertext[0] ^= 0xFF
	_, err = kr.Open(ctx, tamperCiphertext, testContext())
	require.ErrorIs(t, err, ErrOpenFailed)

	tamperNonce := clone(base)
	tamperNonce.Nonce[0] ^= 0xFF
	_, err = kr.Open(ctx, tamperNonce, testContext())
	require.ErrorIs(t, err, ErrOpenFailed)

	tamperAAD := clone(base)
	tamperAAD.AAD[len(tamperAAD.AAD)-1] ^= 0xFF
	_, err = kr.Open(ctx, tamperAAD, testContext())
	require.ErrorIs(t, err, ErrOpenFailed)
}

func clone(env credentialstore.SecretEnvelope) credentialstore.SecretEnvelope {
	return credentialstore.SecretEnvelope{
		Algorithm:  env.Algorithm,
		KeyID:      env.KeyID,
		KeyVersion: env.KeyVersion,
		Nonce:      append([]byte(nil), env.Nonce...),
		Ciphertext: append([]byte(nil), env.Ciphertext...),
		AAD:        append([]byte(nil), env.AAD...),
	}
}

func TestKeyringKeyVersionSelection(t *testing.T) {
	ctx := context.Background()
	// Two versions of the same key id with distinct material.
	entries := []manifestKey{{"primary", 1, keyMaterial(0x55)}, {"primary", 2, keyMaterial(0x66)}}

	sealV1 := newTestKeyring(t, keyRef{"primary", 1}, entries...)
	envV1, err := sealV1.Seal(ctx, []byte("v1-secret"), testContext())
	require.NoError(t, err)
	require.Equal(t, uint64(1), envV1.KeyVersion)

	sealV2 := newTestKeyring(t, keyRef{"primary", 2}, entries...)
	envV2, err := sealV2.Seal(ctx, []byte("v2-secret"), testContext())
	require.NoError(t, err)
	require.Equal(t, uint64(2), envV2.KeyVersion)

	// A keyring holding both versions opens either revision by its version.
	both := newTestKeyring(t, keyRef{"primary", 2}, entries...)
	openedV1, err := both.Open(ctx, envV1, testContext())
	require.NoError(t, err)
	require.Equal(t, []byte("v1-secret"), openedV1)
	openedV2, err := both.Open(ctx, envV2, testContext())
	require.NoError(t, err)
	require.Equal(t, []byte("v2-secret"), openedV2)

	// A keyring missing v1 cannot open a v1 envelope.
	v2Only := newTestKeyring(t, keyRef{"primary", 2}, manifestKey{"primary", 2, keyMaterial(0x66)})
	_, err = v2Only.Open(ctx, envV1, testContext())
	require.ErrorIs(t, err, ErrOpenFailed)

	has, err := both.HasEnvelopeKey(ctx, "primary", 1)
	require.NoError(t, err)
	require.True(t, has)
	has, err = v2Only.HasEnvelopeKey(ctx, "primary", 1)
	require.NoError(t, err)
	require.False(t, has)
}

func TestKeyringRestartOpensAcrossReload(t *testing.T) {
	ctx := context.Background()
	dir := t.TempDir()
	path := filepath.Join(dir, "manifest.yaml")
	writeFileMode(t, path, manifestYAML(manifestKey{"primary", 1, keyMaterial(0x77)}), 0o600)

	cfg := FileConfig{KeyFile: path, ActiveKeyID: "primary", ActiveKeyVersion: 1}
	kr1, err := LoadFile(cfg)
	require.NoError(t, err)
	require.NotNil(t, kr1)
	env, err := kr1.Seal(ctx, []byte("restart-secret"), testContext())
	require.NoError(t, err)

	// A fresh process reloads the same external file and opens the envelope.
	kr2, err := LoadFile(cfg)
	require.NoError(t, err)
	opened, err := kr2.Open(ctx, env, testContext())
	require.NoError(t, err)
	require.Equal(t, []byte("restart-secret"), opened)
}

func TestKeyringErrorsAndEnvelopeCarryNoSecret(t *testing.T) {
	ctx := context.Background()
	material := keyMaterial(0x88)
	kr := newTestKeyring(t, keyRef{"primary", 1}, manifestKey{"primary", 1, material})

	secret := "PLAINTEXT_SECRET_SENTINEL_1234567890"
	env, err := kr.Seal(ctx, []byte(secret), testContext())
	require.NoError(t, err)

	// No envelope field may carry the plaintext or the raw key material.
	for _, field := range [][]byte{env.Nonce, env.Ciphertext, env.AAD} {
		require.NotContains(t, string(field), secret)
		require.NotContains(t, string(field), material)
	}

	// Constant errors never carry secret material.
	_, openErr := kr.Open(ctx, env, Context{PrincipalID: "wrong"})
	require.ErrorIs(t, openErr, ErrOpenFailed)
	for _, e := range []error{ErrSealFailed, ErrOpenFailed, openErr} {
		require.NotContains(t, e.Error(), secret)
		require.NotContains(t, e.Error(), material)
	}
}

func TestKeyringUnknownVersionAtOpen(t *testing.T) {
	ctx := context.Background()
	kr := newTestKeyring(t, keyRef{"primary", 1}, manifestKey{"primary", 1, keyMaterial(0x99)})
	env, err := kr.Seal(ctx, []byte("secret"), testContext())
	require.NoError(t, err)

	env.KeyVersion = 99
	_, err = kr.Open(ctx, env, testContext())
	require.ErrorIs(t, err, ErrOpenFailed)
}

// ---------------------------------------------------------------------------
// LoadFile: config, manifest, and secure-file behavior
// ---------------------------------------------------------------------------

func TestLoadFileDisabledWhenAllOmitted(t *testing.T) {
	kr, err := LoadFile(FileConfig{})
	require.NoError(t, err)
	require.Nil(t, kr)
}

func TestLoadFilePartialConfigFails(t *testing.T) {
	for name, cfg := range map[string]FileConfig{
		"only-file":    {KeyFile: "/tmp/x"},
		"only-id":      {ActiveKeyID: "primary"},
		"only-version": {ActiveKeyVersion: 1},
		"file-id":      {KeyFile: "/tmp/x", ActiveKeyID: "primary"},
		"file-version": {KeyFile: "/tmp/x", ActiveKeyVersion: 1},
		"id-version":   {ActiveKeyID: "primary", ActiveKeyVersion: 1},
	} {
		t.Run(name, func(t *testing.T) {
			_, err := LoadFile(cfg)
			require.ErrorIs(t, err, ErrConfigIncomplete)
		})
	}
}

func TestLoadFileValidManifest(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "manifest.yaml")
	writeFileMode(t, path, manifestYAML(manifestKey{"primary", 1, keyMaterial(0x01)}), 0o600)

	kr, err := LoadFile(FileConfig{KeyFile: path, ActiveKeyID: "primary", ActiveKeyVersion: 1})
	require.NoError(t, err)
	require.NotNil(t, kr)
	_, err = kr.Seal(context.Background(), []byte("secret"), testContext())
	require.NoError(t, err)
}

func TestLoadFileAtomicSymlinkAndSwap(t *testing.T) {
	ctx := context.Background()
	dir := t.TempDir()

	// Kubernetes-style secret mount: manifest.yaml -> ..data/manifest.yaml,
	// ..data -> a versioned data directory that is swapped atomically.
	dataV1 := filepath.Join(dir, "..2026_01_data")
	require.NoError(t, os.Mkdir(dataV1, 0o700))
	writeFileMode(t, filepath.Join(dataV1, "manifest.yaml"), manifestYAML(manifestKey{"primary", 1, keyMaterial(0x0A)}), 0o600)
	require.NoError(t, os.Symlink(dataV1, filepath.Join(dir, "..data")))
	require.NoError(t, os.Symlink(filepath.Join("..data", "manifest.yaml"), filepath.Join(dir, "manifest.yaml")))

	keyFile := filepath.Join(dir, "manifest.yaml")
	cfg := FileConfig{KeyFile: keyFile, ActiveKeyID: "primary", ActiveKeyVersion: 1}

	krOld, err := LoadFile(cfg)
	require.NoError(t, err)
	require.NotNil(t, krOld)
	envOld, err := krOld.Seal(ctx, []byte("secret"), testContext())
	require.NoError(t, err)

	// Atomically swap ..data to a new versioned directory with different key
	// material for the same id/version.
	dataV2 := filepath.Join(dir, "..2026_02_data")
	require.NoError(t, os.Mkdir(dataV2, 0o700))
	writeFileMode(t, filepath.Join(dataV2, "manifest.yaml"), manifestYAML(manifestKey{"primary", 1, keyMaterial(0x0B)}), 0o600)
	tmpLink := filepath.Join(dir, "..data_tmp")
	require.NoError(t, os.Symlink(dataV2, tmpLink))
	require.NoError(t, os.Rename(tmpLink, filepath.Join(dir, "..data")))

	krNew, err := LoadFile(cfg)
	require.NoError(t, err)
	require.NotNil(t, krNew)
	// The reload picked up the swapped material: the new keyring cannot open a
	// ciphertext sealed under the old material even at the same id/version.
	_, err = krNew.Open(ctx, envOld, testContext())
	require.ErrorIs(t, err, ErrOpenFailed)
}

func TestLoadFileDanglingSymlink(t *testing.T) {
	dir := t.TempDir()
	link := filepath.Join(dir, "manifest.yaml")
	require.NoError(t, os.Symlink(filepath.Join(dir, "nonexistent"), link))

	_, err := LoadFile(FileConfig{KeyFile: link, ActiveKeyID: "primary", ActiveKeyVersion: 1})
	require.ErrorIs(t, err, ErrKeyFileAccess)
}

func TestLoadFileMissingFile(t *testing.T) {
	_, err := LoadFile(FileConfig{KeyFile: filepath.Join(t.TempDir(), "absent.yaml"), ActiveKeyID: "primary", ActiveKeyVersion: 1})
	require.ErrorIs(t, err, ErrKeyFileAccess)
}

func TestLoadFileNonRegularTarget(t *testing.T) {
	dir := t.TempDir()
	sub := filepath.Join(dir, "adir")
	require.NoError(t, os.Mkdir(sub, 0o700))
	_, err := LoadFile(FileConfig{KeyFile: sub, ActiveKeyID: "primary", ActiveKeyVersion: 1})
	require.ErrorIs(t, err, ErrKeyFileInsecure)
}

func TestLoadFileInsecurePermissions(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "manifest.yaml")
	writeFileMode(t, path, manifestYAML(manifestKey{"primary", 1, keyMaterial(0x02)}), 0o644)
	_, err := LoadFile(FileConfig{KeyFile: path, ActiveKeyID: "primary", ActiveKeyVersion: 1})
	require.ErrorIs(t, err, ErrKeyFileInsecure)
}

func TestLoadFileManifestErrors(t *testing.T) {
	dir := t.TempDir()
	cases := map[string]struct {
		body string
		want error
	}{
		"malformed":        {"keys: [this is not valid", ErrManifest},
		"unknown-field":    {manifestYAML(manifestKey{"primary", 1, keyMaterial(0x03)}) + "extra: value\n", ErrManifest},
		"empty":            {"keys: []\n", ErrManifest},
		"duplicate":        {manifestYAML(manifestKey{"primary", 1, keyMaterial(0x03)}, manifestKey{"primary", 1, keyMaterial(0x04)}), ErrManifest},
		"zero-version":     {manifestYAML(manifestKey{"primary", 0, keyMaterial(0x03)}), ErrManifest},
		"empty-id":         {manifestYAML(manifestKey{"", 1, keyMaterial(0x03)}), ErrManifest},
		"bad-key-length":   {manifestYAML(manifestKey{"primary", 1, base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{0x1}, 16))}), ErrKeyMaterial},
		"bad-key-encoding": {manifestYAML(manifestKey{"primary", 1, "not-base64!!!"}), ErrKeyMaterial},
	}
	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			path := filepath.Join(dir, name+".yaml")
			writeFileMode(t, path, tc.body, 0o600)
			_, err := LoadFile(FileConfig{KeyFile: path, ActiveKeyID: "primary", ActiveKeyVersion: 1})
			require.ErrorIs(t, err, tc.want)
		})
	}
}

func TestLoadFileUnknownActiveKey(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "manifest.yaml")
	writeFileMode(t, path, manifestYAML(manifestKey{"primary", 1, keyMaterial(0x05)}), 0o600)
	_, err := LoadFile(FileConfig{KeyFile: path, ActiveKeyID: "primary", ActiveKeyVersion: 9})
	require.ErrorIs(t, err, ErrUnknownActiveKey)
}
