package wasm

import (
	"bytes"
	"context"
	"io"
	"os"
	"path/filepath"
	"testing"

	"Picocrypt-NG/internal/encoding"
	"Picocrypt-NG/internal/header"
	"Picocrypt-NG/internal/volume"
)

func TestDecryptV1(t *testing.T) {
	// Read the v1 test volume (password-only, no keyfiles)
	volumeData, err := os.ReadFile("../../testdata/golden/pico_test_v1.txt.pcv")
	if err != nil {
		t.Fatalf("failed to read test file: %v", err)
	}

	// Decrypt with password "test"
	plaintext, errCode := DecryptVolume(volumeData, "test")
	if errCode != 0 {
		t.Fatalf("decrypt failed with error code %d", errCode)
	}

	// Read expected content
	expected, err := os.ReadFile("../../testdata/golden/pico_test.txt")
	if err != nil {
		t.Fatalf("failed to read expected file: %v", err)
	}

	// Normalize line endings: git may convert \n → \r\n on Windows checkout
	expected = bytes.ReplaceAll(expected, []byte("\r\n"), []byte("\n"))
	if !bytes.Equal(plaintext, expected) {
		t.Errorf("decrypted content doesn't match expected\ngot: %q\nwant: %q", plaintext, expected)
	}
}

func TestDecryptV2(t *testing.T) {
	// Read the v2 test volume (password-only, no keyfiles)
	volumeData, err := os.ReadFile("../../testdata/golden/pico_test_v2.txt.pcv")
	if err != nil {
		t.Fatalf("failed to read test file: %v", err)
	}

	// Decrypt with password "test"
	plaintext, errCode := DecryptVolume(volumeData, "test")
	if errCode != 0 {
		t.Fatalf("decrypt failed with error code %d", errCode)
	}

	// Read expected content
	expected, err := os.ReadFile("../../testdata/golden/pico_test.txt")
	if err != nil {
		t.Fatalf("failed to read expected file: %v", err)
	}

	expected = bytes.ReplaceAll(expected, []byte("\r\n"), []byte("\n"))
	if !bytes.Equal(plaintext, expected) {
		t.Errorf("decrypted content doesn't match expected\ngot: %q\nwant: %q", plaintext, expected)
	}
}

func TestDecryptWrongPassword(t *testing.T) {
	volumeData, err := os.ReadFile("../../testdata/golden/pico_test_v2.txt.pcv")
	if err != nil {
		t.Fatalf("failed to read test file: %v", err)
	}

	_, errCode := DecryptVolume(volumeData, "wrongpassword")
	if errCode != ErrWrongPassword {
		t.Errorf("expected error code %d, got %d", ErrWrongPassword, errCode)
	}
}

func TestEncryptDecryptRoundtrip(t *testing.T) {
	original := []byte("Hello, Picocrypt-NG WASM world!")
	password := "testpassword123"

	// Encrypt
	ciphertext, errCode := EncryptVolume(original, password)
	if errCode != 0 {
		t.Fatalf("encrypt failed with error code %d", errCode)
	}

	// Decrypt
	plaintext, errCode := DecryptVolume(ciphertext, password)
	if errCode != 0 {
		t.Fatalf("decrypt failed with error code %d", errCode)
	}

	if !bytes.Equal(plaintext, original) {
		t.Errorf("roundtrip failed\ngot: %q\nwant: %q", plaintext, original)
	}
}

func TestEncryptDecryptLargerFile(t *testing.T) {
	// Create a larger test file (100KB)
	original := make([]byte, 100*1024)
	for i := range original {
		original[i] = byte(i % 256)
	}
	password := "testpassword123"

	// Encrypt
	ciphertext, errCode := EncryptVolume(original, password)
	if errCode != 0 {
		t.Fatalf("encrypt failed with error code %d", errCode)
	}

	// Decrypt
	plaintext, errCode := DecryptVolume(ciphertext, password)
	if errCode != 0 {
		t.Fatalf("decrypt failed with error code %d", errCode)
	}

	if !bytes.Equal(plaintext, original) {
		t.Errorf("roundtrip failed for larger file")
	}
}

func TestWASMUsesWriteAuthValues(t *testing.T) {
	originalWriteAuthValues := writeAuthValues
	defer func() {
		writeAuthValues = originalWriteAuthValues
	}()

	sentinelKeyHash := bytes.Repeat([]byte{0xA1}, header.KeyHashSize)
	sentinelKeyfileHash := bytes.Repeat([]byte{0xB2}, header.KeyfileHashSize)
	sentinelAuthTag := bytes.Repeat([]byte{0xC3}, header.AuthTagSize)
	var calls int

	writeAuthValues = func(w io.WriterAt, offset int64, keyHash, keyfileHash, authTag []byte, rs *encoding.RSCodecs) error {
		calls++
		if offset != header.AuthValuesOffset(0) {
			t.Errorf("auth values offset = %d; want %d", offset, header.AuthValuesOffset(0))
		}
		if len(keyHash) != header.KeyHashSize {
			t.Errorf("keyHash size = %d; want %d", len(keyHash), header.KeyHashSize)
		}
		if len(keyfileHash) != header.KeyfileHashSize {
			t.Errorf("keyfileHash size = %d; want %d", len(keyfileHash), header.KeyfileHashSize)
		}
		if len(authTag) != header.AuthTagSize {
			t.Errorf("authTag size = %d; want %d", len(authTag), header.AuthTagSize)
		}

		return originalWriteAuthValues(w, offset, sentinelKeyHash, sentinelKeyfileHash, sentinelAuthTag, rs)
	}

	volumeData, errCode := EncryptVolume([]byte("phase 6 wasm auth writer guard"), "phase6-password")
	if errCode != 0 {
		t.Fatalf("EncryptVolume returned error code %d", errCode)
	}
	if calls != 1 {
		t.Fatalf("writeAuthValues calls = %d; want 1", calls)
	}

	rs, err := encoding.NewRSCodecs()
	if err != nil {
		t.Fatalf("NewRSCodecs failed: %v", err)
	}
	result, err := header.NewReader(bytes.NewReader(volumeData), rs).ReadHeader()
	if err != nil {
		t.Fatalf("ReadHeader failed: %v", err)
	}

	if !bytes.Equal(result.Header.KeyHash, sentinelKeyHash) {
		t.Fatal("WASM header key hash was not written by writeAuthValues")
	}
	if !bytes.Equal(result.Header.KeyfileHash, sentinelKeyfileHash) {
		t.Fatal("WASM header keyfile hash was not written by writeAuthValues")
	}
	if !bytes.Equal(result.Header.AuthTag, sentinelAuthTag) {
		t.Fatal("WASM header auth tag was not written by writeAuthValues")
	}
}

func TestWASMRoundtripDesktopDecrypt(t *testing.T) {
	original := []byte("Phase 6 WASM standard volume decrypts through the shared desktop volume path.")
	password := "phase6-desktop-interop"

	volumeData, errCode := EncryptVolume(original, password)
	if errCode != 0 {
		t.Fatalf("EncryptVolume returned error code %d", errCode)
	}

	tmpDir := t.TempDir()
	encryptedPath := filepath.Join(tmpDir, "wasm-output.pcv")
	decryptedPath := filepath.Join(tmpDir, "desktop-output.txt")
	if err := os.WriteFile(encryptedPath, volumeData, 0600); err != nil {
		t.Fatalf("write encrypted volume: %v", err)
	}

	rsCodecs, err := encoding.NewRSCodecs()
	if err != nil {
		t.Fatalf("NewRSCodecs failed: %v", err)
	}

	if err := volume.Decrypt(context.Background(), &volume.DecryptRequest{
		InputFile:  encryptedPath,
		OutputFile: decryptedPath,
		Password:   password,
		RSCodecs:   rsCodecs,
	}); err != nil {
		t.Fatalf("volume.Decrypt failed: %v", err)
	}

	plaintext, err := os.ReadFile(decryptedPath)
	if err != nil {
		t.Fatalf("read decrypted output: %v", err)
	}
	if !bytes.Equal(plaintext, original) {
		t.Fatalf("desktop decrypt output mismatch\ngot:  %q\nwant: %q", plaintext, original)
	}
}

func observeWASMZeroingForTest(observer func(wasmZeroingEvent)) func() {
	previous := wasmZeroingObserver
	wasmZeroingObserver = observer
	return func() {
		wasmZeroingObserver = previous
	}
}

func TestWASMBuffersZeroed(t *testing.T) {
	plaintext := []byte("Phase 6 zeroing coverage needs plaintext-derived ciphertext staging.")
	password := "phase6-zeroing-password"

	var events []wasmZeroingEvent
	restore := observeWASMZeroingForTest(func(event wasmZeroingEvent) {
		events = append(events, event)
	})
	defer restore()

	volumeData, errCode := EncryptVolume(plaintext, password)
	if errCode != 0 {
		t.Fatalf("EncryptVolume returned error code %d", errCode)
	}
	if len(volumeData) == 0 {
		t.Fatal("EncryptVolume returned empty volume")
	}
	if bytes.Equal(volumeData, make([]byte, len(volumeData))) {
		t.Fatal("returned volume bytes were wiped before return")
	}

	// The plaintext argument is caller-owned at the internal package boundary.
	// Browser bridge input-copy zeroing is covered by the bridge task; this test
	// only observes encryption-local Go buffers that EncryptVolume owns.
	if !bytes.Equal(plaintext, []byte("Phase 6 zeroing coverage needs plaintext-derived ciphertext staging.")) {
		t.Fatal("EncryptVolume wiped caller-owned plaintext input")
	}

	want := map[wasmZeroingBufferKind]bool{
		wasmZeroingPasswordBytes:    true,
		wasmZeroingHeaderSubkey:     true,
		wasmZeroingMACSubkey:        true,
		wasmZeroingSerpentKey:       true,
		wasmZeroingCiphertextChunk:  true,
		wasmZeroingCiphertextBuffer: true,
		wasmZeroingKeyfileHash:      true,
		wasmZeroingHeaderKeyHash:    true,
		wasmZeroingAuthTag:          true,
		wasmZeroingHeaderBuffer:     true,
	}
	seen := make(map[wasmZeroingBufferKind]wasmZeroingEvent)
	for _, event := range events {
		if event.Len == 0 {
			t.Fatalf("%s zeroing event had empty buffer", event.Kind)
		}
		if !event.Zeroed {
			t.Fatalf("%s buffer was not zeroed after cleanup", event.Kind)
		}
		seen[event.Kind] = event
	}
	for kind := range want {
		if _, ok := seen[kind]; !ok {
			t.Fatalf("missing zeroing event for %s; saw %v", kind, seen)
		}
	}

	for _, kind := range []wasmZeroingBufferKind{
		wasmZeroingPasswordBytes,
		wasmZeroingHeaderSubkey,
		wasmZeroingMACSubkey,
		wasmZeroingSerpentKey,
		wasmZeroingCiphertextChunk,
		wasmZeroingCiphertextBuffer,
		wasmZeroingHeaderKeyHash,
		wasmZeroingAuthTag,
		wasmZeroingHeaderBuffer,
	} {
		if !seen[kind].WasNonZero {
			t.Fatalf("%s buffer was already zero before cleanup; test would be vacuous", kind)
		}
	}
}

func TestWASMUnsupportedFeatureFlagsReturnUnsupported(t *testing.T) {
	cases := []struct {
		name  string
		flags header.Flags
	}{
		{
			name:  "keyfiles",
			flags: header.Flags{UseKeyfiles: true},
		},
		{
			name:  "keyfile_ordered",
			flags: header.Flags{KeyfileOrdered: true},
		},
		{
			name:  "reed_solomon_payload",
			flags: header.Flags{ReedSolomon: true},
		},
		{
			name:  "reed_solomon_padding",
			flags: header.Flags{Padded: true},
		},
		{
			name:  "paranoid",
			flags: header.Flags{Paranoid: true},
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			volumeData := wasmVolumeWithFlags(t, tc.flags)

			_, errCode := DecryptVolume(volumeData, "phase6-unsupported-flags")
			if errCode != ErrUnsupported {
				t.Fatalf("DecryptVolume error code = %d; want ErrUnsupported", errCode)
			}
		})
	}
}

func TestWASMDecryptBuffersZeroed(t *testing.T) {
	original := []byte("Phase 6 decrypt zeroing coverage needs returned plaintext intact.")
	password := "phase6-decrypt-zeroing"
	volumeData, errCode := EncryptVolume(original, password)
	if errCode != 0 {
		t.Fatalf("EncryptVolume returned error code %d", errCode)
	}

	var events []wasmZeroingEvent
	restore := observeWASMZeroingForTest(func(event wasmZeroingEvent) {
		events = append(events, event)
	})
	defer restore()

	plaintext, errCode := DecryptVolume(volumeData, password)
	if errCode != 0 {
		t.Fatalf("DecryptVolume returned error code %d", errCode)
	}
	if !bytes.Equal(plaintext, original) {
		t.Fatalf("plaintext mismatch\ngot:  %q\nwant: %q", plaintext, original)
	}
	if bytes.Equal(plaintext, make([]byte, len(plaintext))) {
		t.Fatal("returned plaintext was wiped before return")
	}

	// This observes package-owned Go slices only. JavaScript engine copies and
	// Go runtime/GC-managed copies cannot be inspected directly from this test.
	want := map[wasmZeroingBufferKind]bool{
		wasmZeroingDecryptPasswordBytes:  true,
		wasmZeroingDecryptKeyfileHash:    false,
		wasmZeroingDecryptHeaderSubkey:   true,
		wasmZeroingDecryptMACSubkey:      true,
		wasmZeroingDecryptSerpentKey:     true,
		wasmZeroingDecryptPlaintextChunk: true,
		wasmZeroingDecryptComputedMAC:    true,
	}
	seen := make(map[wasmZeroingBufferKind]wasmZeroingEvent)
	for _, event := range events {
		if event.Len == 0 {
			t.Fatalf("%s zeroing event had empty buffer", event.Kind)
		}
		if !event.Zeroed {
			t.Fatalf("%s buffer was not zeroed after cleanup", event.Kind)
		}
		seen[event.Kind] = event
	}
	for kind, mustBeNonZero := range want {
		event, ok := seen[kind]
		if !ok {
			t.Fatalf("missing zeroing event for %s; saw %v", kind, seen)
		}
		if mustBeNonZero && !event.WasNonZero {
			t.Fatalf("%s buffer was already zero before cleanup; test would be vacuous", kind)
		}
	}
}

func wasmVolumeWithFlags(t *testing.T, flags header.Flags) []byte {
	t.Helper()

	volumeData, errCode := EncryptVolume([]byte("unsupported wasm feature flags"), "phase6-unsupported-flags")
	if errCode != 0 {
		t.Fatalf("EncryptVolume returned error code %d", errCode)
	}

	rs, err := encoding.NewRSCodecs()
	if err != nil {
		t.Fatalf("NewRSCodecs failed: %v", err)
	}
	flagsEnc, err := encoding.Encode(rs.RS5, flags.ToBytes())
	if err != nil {
		t.Fatalf("encode flags: %v", err)
	}

	patched := append([]byte(nil), volumeData...)
	flagsOffset := header.VersionEncSize + header.CommentLenEncSize
	copy(patched[flagsOffset:flagsOffset+header.FlagsEncSize], flagsEnc)
	return patched
}
