package algo

import (
	"bytes"
	"testing"
)

func TestIndexByteTwo(t *testing.T) {
	tests := []struct {
		name string
		s    string
		b1   byte
		b2   byte
		want int
	}{
		{"empty", "", 'a', 'b', -1},
		{"single_b1", "a", 'a', 'b', 0},
		{"single_b2", "b", 'a', 'b', 0},
		{"single_none", "c", 'a', 'b', -1},
		{"b1_first", "xaxb", 'a', 'b', 1},
		{"b2_first", "xbxa", 'a', 'b', 1},
		{"same_byte", "xxa", 'a', 'a', 2},
		{"at_end", "xxxxa", 'a', 'b', 4},
		{"not_found", "xxxxxxxx", 'a', 'b', -1},
		{"long_b1_at_3000", string(make([]byte, 3000)) + "a" + string(make([]byte, 1000)), 'a', 'b', 3000},
		{"long_b2_at_3000", string(make([]byte, 3000)) + "b" + string(make([]byte, 1000)), 'a', 'b', 3000},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := IndexByteTwo([]byte(tt.s), tt.b1, tt.b2)
			if got != tt.want {
				t.Errorf("IndexByteTwo(%q, %c, %c) = %d, want %d", tt.s[:min(len(tt.s), 40)], tt.b1, tt.b2, got, tt.want)
			}
		})
	}

	// Exhaustive test: compare against loop reference for various lengths,
	// including sizes around SIMD block boundaries (16, 32, 64).
	for n := 0; n <= 256; n++ {
		data := make([]byte, n)
		for i := range data {
			data[i] = byte('c' + (i % 20))
		}
		// Test with match at every position
		for pos := 0; pos < n; pos++ {
			for _, b := range []byte{'A', 'B'} {
				data[pos] = b
				got := IndexByteTwo(data, 'A', 'B')
				want := loopIndexByteTwo(data, 'A', 'B')
				if got != want {
					t.Fatalf("IndexByteTwo(len=%d, match=%c@%d) = %d, want %d", n, b, pos, got, want)
				}
				data[pos] = byte('c' + (pos % 20))
			}
		}
		// Test with no match
		got := IndexByteTwo(data, 'A', 'B')
		if got != -1 {
			t.Fatalf("IndexByteTwo(len=%d, no match) = %d, want -1", n, got)
		}
		// Test with both bytes present
		if n >= 2 {
			data[n/3] = 'A'
			data[n*2/3] = 'B'
			got := IndexByteTwo(data, 'A', 'B')
			want := loopIndexByteTwo(data, 'A', 'B')
			if got != want {
				t.Fatalf("IndexByteTwo(len=%d, both@%d,%d) = %d, want %d", n, n/3, n*2/3, got, want)
			}
			data[n/3] = byte('c' + ((n / 3) % 20))
			data[n*2/3] = byte('c' + ((n * 2 / 3) % 20))
		}
	}
}

func TestLastIndexByteTwo(t *testing.T) {
	tests := []struct {
		name string
		s    string
		b1   byte
		b2   byte
		want int
	}{
		{"empty", "", 'a', 'b', -1},
		{"single_b1", "a", 'a', 'b', 0},
		{"single_b2", "b", 'a', 'b', 0},
		{"single_none", "c", 'a', 'b', -1},
		{"b1_last", "xbxa", 'a', 'b', 3},
		{"b2_last", "xaxb", 'a', 'b', 3},
		{"same_byte", "axx", 'a', 'a', 0},
		{"at_start", "axxxx", 'a', 'b', 0},
		{"both_present", "axbx", 'a', 'b', 2},
		{"not_found", "xxxxxxxx", 'a', 'b', -1},
		{"long_b1_at_3000", string(make([]byte, 3000)) + "a" + string(make([]byte, 1000)), 'a', 'b', 3000},
		{"long_b2_at_end", string(make([]byte, 4000)) + "b", 'a', 'b', 4000},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := lastIndexByteTwo([]byte(tt.s), tt.b1, tt.b2)
			if got != tt.want {
				t.Errorf("lastIndexByteTwo(%q, %c, %c) = %d, want %d", tt.s[:min(len(tt.s), 40)], tt.b1, tt.b2, got, tt.want)
			}
		})
	}

	// Exhaustive test against loop reference
	for n := 0; n <= 256; n++ {
		data := make([]byte, n)
		for i := range data {
			data[i] = byte('c' + (i % 20))
		}
		for pos := 0; pos < n; pos++ {
			for _, b := range []byte{'A', 'B'} {
				data[pos] = b
				got := lastIndexByteTwo(data, 'A', 'B')
				want := refLastIndexByteTwo(data, 'A', 'B')
				if got != want {
					t.Fatalf("lastIndexByteTwo(len=%d, match=%c@%d) = %d, want %d", n, b, pos, got, want)
				}
				data[pos] = byte('c' + (pos % 20))
			}
		}
		// No match
		got := lastIndexByteTwo(data, 'A', 'B')
		if got != -1 {
			t.Fatalf("lastIndexByteTwo(len=%d, no match) = %d, want -1", n, got)
		}
		// Both bytes present
		if n >= 2 {
			data[n/3] = 'A'
			data[n*2/3] = 'B'
			got := lastIndexByteTwo(data, 'A', 'B')
			want := refLastIndexByteTwo(data, 'A', 'B')
			if got != want {
				t.Fatalf("lastIndexByteTwo(len=%d, both@%d,%d) = %d, want %d", n, n/3, n*2/3, got, want)
			}
			data[n/3] = byte('c' + ((n / 3) % 20))
			data[n*2/3] = byte('c' + ((n * 2 / 3) % 20))
		}
	}
}

func FuzzIndexByteTwo(f *testing.F) {
	f.Add([]byte("hello world"), byte('o'), byte('l'))
	f.Add([]byte(""), byte('a'), byte('b'))
	f.Add([]byte("aaa"), byte('a'), byte('a'))
	f.Fuzz(func(t *testing.T, data []byte, b1, b2 byte) {
		got := IndexByteTwo(data, b1, b2)
		want := loopIndexByteTwo(data, b1, b2)
		if got != want {
			t.Errorf("IndexByteTwo(len=%d, b1=%d, b2=%d) = %d, want %d", len(data), b1, b2, got, want)
		}
	})
}

func FuzzLastIndexByteTwo(f *testing.F) {
	f.Add([]byte("hello world"), byte('o'), byte('l'))
	f.Add([]byte(""), byte('a'), byte('b'))
	f.Add([]byte("aaa"), byte('a'), byte('a'))
	f.Fuzz(func(t *testing.T, data []byte, b1, b2 byte) {
		got := lastIndexByteTwo(data, b1, b2)
		want := refLastIndexByteTwo(data, b1, b2)
		if got != want {
			t.Errorf("lastIndexByteTwo(len=%d, b1=%d, b2=%d) = %d, want %d", len(data), b1, b2, got, want)
		}
	})
}

// Reference implementations for correctness checking
func refIndexByteTwo(s []byte, b1, b2 byte) int {
	i1 := bytes.IndexByte(s, b1)
	if i1 == 0 {
		return 0
	}
	scope := s
	if i1 > 0 {
		scope = s[:i1]
	}
	if i2 := bytes.IndexByte(scope, b2); i2 >= 0 {
		return i2
	}
	return i1
}

func loopIndexByteTwo(s []byte, b1, b2 byte) int {
	for i, b := range s {
		if b == b1 || b == b2 {
			return i
		}
	}
	return -1
}

func refLastIndexByteTwo(s []byte, b1, b2 byte) int {
	for i := len(s) - 1; i >= 0; i-- {
		if s[i] == b1 || s[i] == b2 {
			return i
		}
	}
	return -1
}

func benchIndexByteTwo(b *testing.B, size int, pos int) {
	data := make([]byte, size)
	for i := range data {
		data[i] = byte('a' + (i % 20))
	}
	data[pos] = 'Z'

	type impl struct {
		name string
		fn   func([]byte, byte, byte) int
	}
	impls := []impl{
		{"asm", IndexByteTwo},
		{"2xIndexByte", refIndexByteTwo},
		{"loop", loopIndexByteTwo},
	}
	for _, im := range impls {
		b.Run(im.name, func(b *testing.B) {
			for i := 0; i < b.N; i++ {
				im.fn(data, 'Z', 'z')
			}
		})
	}
}

func benchLastIndexByteTwo(b *testing.B, size int, pos int) {
	data := make([]byte, size)
	for i := range data {
		data[i] = byte('a' + (i % 20))
	}
	data[pos] = 'Z'

	type impl struct {
		name string
		fn   func([]byte, byte, byte) int
	}
	impls := []impl{
		{"asm", lastIndexByteTwo},
		{"loop", refLastIndexByteTwo},
	}
	for _, im := range impls {
		b.Run(im.name, func(b *testing.B) {
			for i := 0; i < b.N; i++ {
				im.fn(data, 'Z', 'z')
			}
		})
	}
}

func BenchmarkIndexByteTwo_10(b *testing.B)       { benchIndexByteTwo(b, 10, 8) }
func BenchmarkIndexByteTwo_100(b *testing.B)      { benchIndexByteTwo(b, 100, 80) }
func BenchmarkIndexByteTwo_1000(b *testing.B)     { benchIndexByteTwo(b, 1000, 800) }
func BenchmarkLastIndexByteTwo_10(b *testing.B)   { benchLastIndexByteTwo(b, 10, 2) }
func BenchmarkLastIndexByteTwo_100(b *testing.B)  { benchLastIndexByteTwo(b, 100, 20) }
func BenchmarkLastIndexByteTwo_1000(b *testing.B) { benchLastIndexByteTwo(b, 1000, 200) }
