package engine

import (
	"errors"
	"testing"

	"github.com/iliadenisov/alphabet"
	dawg "github.com/iliadenisov/dafsa"
)

// buildFinderB builds an in-memory finder over idx from the given words, each
// expressed as alphabet-index bytes. The words must be supplied in strictly
// increasing alphabet-index order, as the builder requires.
func buildFinderB(t *testing.T, idx alphabet.Indexer, words ...[]byte) dawg.Finder {
	t.Helper()
	b := dawg.New(idx)
	for _, w := range words {
		if err := b.AddB(w); err != nil {
			t.Fatalf("addB %v: %v", w, err)
		}
	}
	return b.Finish()
}

// mustDecode decodes index bytes through idx or fails the test.
func mustDecode(t *testing.T, idx alphabet.Indexer, word []byte) string {
	t.Helper()
	s, err := idx.Decode(word)
	if err != nil {
		t.Fatalf("decode %v: %v", word, err)
	}
	return s
}

func equalStrings(a, b []string) bool {
	if len(a) != len(b) {
		return false
	}
	for i := range a {
		if a[i] != b[i] {
			return false
		}
	}
	return true
}

// TestDiffWords checks that DiffWords reports the words present only in the new
// dictionary as added and those present only in the old one as removed, decoded
// to characters through the variant's alphabet.
func TestDiffWords(t *testing.T) {
	rs, _ := VariantEnglish.ruleset()
	idx := rs.Alphabet

	old := buildFinderB(t, idx, []byte{0}, []byte{0, 1}, []byte{2})
	defer func() { _ = old.Close() }()
	updated := buildFinderB(t, idx, []byte{0}, []byte{2}, []byte{2, 3})
	defer func() { _ = updated.Close() }()

	diff, err := DiffWords(VariantEnglish, old, updated)
	if err != nil {
		t.Fatalf("DiffWords: %v", err)
	}

	wantAdded := []string{mustDecode(t, idx, []byte{2, 3})}
	wantRemoved := []string{mustDecode(t, idx, []byte{0, 1})}
	if !equalStrings(diff.Added, wantAdded) {
		t.Errorf("added = %v, want %v", diff.Added, wantAdded)
	}
	if !equalStrings(diff.Removed, wantRemoved) {
		t.Errorf("removed = %v, want %v", diff.Removed, wantRemoved)
	}
}

// TestDiffWordsIdentical checks that comparing a dictionary with itself yields an
// empty diff.
func TestDiffWordsIdentical(t *testing.T) {
	rs, _ := VariantEnglish.ruleset()
	f := buildFinderB(t, rs.Alphabet, []byte{0}, []byte{1}, []byte{1, 2})
	defer func() { _ = f.Close() }()

	diff, err := DiffWords(VariantEnglish, f, f)
	if err != nil {
		t.Fatalf("DiffWords: %v", err)
	}
	if len(diff.Added) != 0 || len(diff.Removed) != 0 {
		t.Errorf("diff = %+v, want empty", diff)
	}
}

// TestDiffWordsUnknownVariant checks that an unrecognised variant is rejected.
func TestDiffWordsUnknownVariant(t *testing.T) {
	rs, _ := VariantEnglish.ruleset()
	f := buildFinderB(t, rs.Alphabet, []byte{0})
	defer func() { _ = f.Close() }()
	if _, err := DiffWords(Variant(250), f, f); !errors.Is(err, ErrUnknownVariant) {
		t.Errorf("err = %v, want ErrUnknownVariant", err)
	}
}

// TestOpenFinder checks that OpenFinder loads a committed DAWG from a directory.
func TestOpenFinder(t *testing.T) {
	f, err := OpenFinder(testDictDir(), VariantEnglish)
	if err != nil {
		t.Fatalf("OpenFinder: %v", err)
	}
	defer func() { _ = f.Close() }()
	if f.NumAdded() == 0 {
		t.Error("english dictionary is empty")
	}
}

// TestRegistryFinder checks that Finder returns the resident finder for a pair
// and the right sentinel when the version is absent.
func TestRegistryFinder(t *testing.T) {
	if _, err := testReg.Finder(VariantEnglish, testVersion); err != nil {
		t.Errorf("Finder(scrabble_en, %q): %v", testVersion, err)
	}
	if _, err := testReg.Finder(VariantEnglish, "absent"); !errors.Is(err, ErrUnknownVersion) {
		t.Errorf("Finder absent version err = %v, want ErrUnknownVersion", err)
	}
}

// TestDictFiles checks that DictFiles exposes the variant filename map as a copy
// the caller cannot use to mutate the engine's own map.
func TestDictFiles(t *testing.T) {
	files := DictFiles()
	if len(files) != len(Variants()) {
		t.Fatalf("DictFiles has %d entries, want %d", len(files), len(Variants()))
	}
	files[VariantEnglish] = "tampered"
	if again := DictFiles(); again[VariantEnglish] == "tampered" {
		t.Error("DictFiles returned a shared map; mutating it changed the engine's map")
	}
}
