package handlers

import (
	"context"
	"crypto/rand"
	"encoding/json"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/yan1h/agent-coding-workflow/internal/infra/crypto"
	"github.com/yan1h/agent-coding-workflow/internal/jobs"
)

// memRow models one encrypted-column row across versions.
type memRow struct {
	blob       []byte
	keyVersion int
	plaintext  []byte // ground truth for assertions
	null       bool
}

// memStore is an in-memory ReencryptStore over a single logical table, with a
// failNext hook to model a crash mid-run for the resumability test.
type memStore struct {
	rows     []*memRow
	failNext bool
}

func (m *memStore) Tables() []string { return []string{"t"} }

func (m *memStore) StaleCount(_ context.Context, _ string, active int) (int, error) {
	n := 0
	for _, r := range m.rows {
		if !r.null && r.keyVersion < active {
			n++
		}
	}
	return n, nil
}

func (m *memStore) ReencryptTable(ctx context.Context, enc *crypto.KeyedEncryptor, _ string, active, batch int) (int, error) {
	done := 0
	for _, r := range m.rows {
		if done >= batch {
			break
		}
		if r.null || r.keyVersion >= active {
			continue
		}
		if m.failNext {
			m.failNext = false
			return done, context.Canceled // simulate a crash after some rows
		}
		plain, err := enc.DecryptVersion(ctx, r.blob, r.keyVersion)
		if err != nil {
			return done, err
		}
		sealed, err := enc.SealWithVersion(ctx, active, plain)
		if err != nil {
			return done, err
		}
		r.blob = sealed
		r.keyVersion = active
		done++
	}
	return done, nil
}

func key(t *testing.T) []byte {
	t.Helper()
	k := make([]byte, 32)
	_, err := rand.Read(k)
	require.NoError(t, err)
	return k
}

// twoVersionEnc returns a KeyedEncryptor with v1+v2 keys and a store reporting
// active=2, plus a helper to seal under v1.
func twoVersionEnc(t *testing.T) *crypto.KeyedEncryptor {
	prov := crypto.NewEnvProvider(nil)
	prov.SetKey(1, key(t))
	prov.SetKey(2, key(t))
	return crypto.NewKeyedEncryptor(prov, crypto.NewStaticKeyStore(2))
}

func TestSecretReencrypt_AdvancesAllRows(t *testing.T) {
	ctx := context.Background()
	enc := twoVersionEnc(t)

	// Seed 3 rows encrypted under v1 + 1 NULL row.
	store := &memStore{}
	for i := 0; i < 3; i++ {
		pt := []byte{byte(i), byte(i + 1)}
		ct, err := enc.SealWithVersion(ctx, 1, pt)
		require.NoError(t, err)
		store.rows = append(store.rows, &memRow{blob: ct, keyVersion: 1, plaintext: pt})
	}
	store.rows = append(store.rows, &memRow{null: true, keyVersion: 1})

	h := NewSecretReencryptHandler(store, enc, nil)
	payload, _ := json.Marshal(SecretReencryptPayload{ActiveVersion: 2, BatchSize: 2})
	require.NoError(t, h.Handle(ctx, jobs.Job{Payload: payload}))

	// All non-null rows now on v2 and decrypt to identical plaintext.
	for _, r := range store.rows {
		if r.null {
			require.Equal(t, 1, r.keyVersion, "NULL rows are skipped, version unchanged")
			continue
		}
		require.Equal(t, 2, r.keyVersion)
		got, err := enc.DecryptVersion(ctx, r.blob, 2)
		require.NoError(t, err)
		require.Equal(t, r.plaintext, got)
	}
	left, _ := store.StaleCount(ctx, "t", 2)
	require.Equal(t, 0, left)
}

func TestSecretReencrypt_IdempotentRerun(t *testing.T) {
	ctx := context.Background()
	enc := twoVersionEnc(t)
	store := &memStore{}
	pt := []byte("hello")
	ct, err := enc.SealWithVersion(ctx, 1, pt)
	require.NoError(t, err)
	store.rows = append(store.rows, &memRow{blob: ct, keyVersion: 1, plaintext: pt})

	h := NewSecretReencryptHandler(store, enc, nil)
	payload, _ := json.Marshal(SecretReencryptPayload{ActiveVersion: 2})
	require.NoError(t, h.Handle(ctx, jobs.Job{Payload: payload}))
	blobAfterFirst := store.rows[0].blob

	// Second run is a no-op: row already on active version, blob unchanged.
	require.NoError(t, h.Handle(ctx, jobs.Job{Payload: payload}))
	require.Equal(t, blobAfterFirst, store.rows[0].blob)
}

func TestSecretReencrypt_ResumableAfterCrash(t *testing.T) {
	ctx := context.Background()
	enc := twoVersionEnc(t)
	store := &memStore{failNext: true}
	for i := 0; i < 5; i++ {
		pt := []byte{byte(i)}
		ct, err := enc.SealWithVersion(ctx, 1, pt)
		require.NoError(t, err)
		store.rows = append(store.rows, &memRow{blob: ct, keyVersion: 1, plaintext: pt})
	}

	h := NewSecretReencryptHandler(store, enc, nil)
	payload, _ := json.Marshal(SecretReencryptPayload{ActiveVersion: 2, BatchSize: 10})
	// First run crashes (ctx.Canceled bubbles up).
	require.Error(t, h.Handle(ctx, jobs.Job{Payload: payload}))
	left, _ := store.StaleCount(ctx, "t", 2)
	require.Greater(t, left, 0, "crash left some rows on old version")

	// Re-run completes the rest.
	require.NoError(t, h.Handle(ctx, jobs.Job{Payload: payload}))
	left, _ = store.StaleCount(ctx, "t", 2)
	require.Equal(t, 0, left)
	for _, r := range store.rows {
		got, err := enc.DecryptVersion(ctx, r.blob, 2)
		require.NoError(t, err)
		require.Equal(t, r.plaintext, got)
	}
}

func TestSecretReencrypt_BadPayloadIsPermanent(t *testing.T) {
	h := NewSecretReencryptHandler(&memStore{}, twoVersionEnc(t), nil)
	err := h.Handle(context.Background(), jobs.Job{Payload: []byte("not json")})
	require.Error(t, err)
	require.True(t, jobs.IsPermanent(err))
}
