package jp

import (
	"bytes"
	"compress/gzip"
	_ "embed"
	"encoding/json"
	"os"
	"reflect"
	"testing"
)

func TestScriptPredicates(t *testing.T) {
	// Hiragana
	for _, r := range []rune{'あ', 'ん', 'ぁ', 'ゝ', 'ゞ'} {
		if !IsHiragana(r) {
			t.Errorf("IsHiragana(%c) = false, want true", r)
		}
	}
	for _, r := range []rune{'ア', '漢', 'a', '1', 'ー'} {
		if IsHiragana(r) {
			t.Errorf("IsHiragana(%c) = true, want false", r)
		}
	}

	// Katakana
	for _, r := range []rune{'ア', 'ン', 'ァ', 'ー', 'ｰ', 'ｱ', 'ﾝ', 'ヽ', 'ヾ'} {
		if !IsKatakana(r) {
			t.Errorf("IsKatakana(%c) = false, want true", r)
		}
	}
	for _, r := range []rune{'あ', '漢', 'a', '1'} {
		if IsKatakana(r) {
			t.Errorf("IsKatakana(%c) = true, want false", r)
		}
	}

	// Kana
	for _, r := range []rune{'あ', 'ア', 'ー', 'ｱ'} {
		if !IsKana(r) {
			t.Errorf("IsKana(%c) = false, want true", r)
		}
	}
	for _, r := range []rune{'漢', 'a', '1'} {
		if IsKana(r) {
			t.Errorf("IsKana(%c) = true, want false", r)
		}
	}

	// Kanji: CJK unified ideographs + ext A + 々〆ヵヶ
	for _, r := range []rune{'漢', '字', '々', '〆', 'ヵ', 'ヶ', rune(0x3400), rune(0x4DBF)} {
		if !IsKanji(r) {
			t.Errorf("IsKanji(%c / %U) = false, want true", r, r)
		}
	}
	for _, r := range []rune{'あ', 'ア', 'a', '1', ' '} {
		if IsKanji(r) {
			t.Errorf("IsKanji(%c) = true, want false", r)
		}
	}

	// HasJapanese
	if !HasJapanese("hello 世界") {
		t.Errorf("HasJapanese('hello 世界') = false, want true")
	}
	if !HasJapanese("こんにちは") {
		t.Errorf("HasJapanese('こんにちは') = false, want true")
	}
	if !HasJapanese("ラーメン") {
		t.Errorf("HasJapanese('ラーメン') = false, want true")
	}
	if HasJapanese("hello world 123 !?") {
		t.Errorf("HasJapanese('hello world 123 !?') = true, want false")
	}

	// IsKanaOnly
	if !IsKanaOnly("こんにちは") {
		t.Errorf("IsKanaOnly('こんにちは') = false, want true")
	}
	if !IsKanaOnly("ラーメン") {
		t.Errorf("IsKanaOnly('ラーメン') = false, want true")
	}
	if IsKanaOnly("こんにちは、世界！") {
		t.Errorf("IsKanaOnly('こんにちは、世界！') = true, want false")
	}
	if !IsKanaOnly("こんにちは！") {
		t.Errorf("IsKanaOnly('こんにちは！') = false, want true")
	}
	if IsKanaOnly("ーーー") {
		t.Errorf("IsKanaOnly('ーーー') = true, want false (no kana letters)")
	}
	if IsKanaOnly("hello") {
		t.Errorf("IsKanaOnly('hello') = true, want false")
	}
}

func TestKanaConversion(t *testing.T) {
	if got := ToHiragana("ラーメン"); got != "らーめん" {
		t.Errorf("ToHiragana(ラーメン) = %q, want %q", got, "らーめん")
	}
	if got := ToKatakana("らーめん"); got != "ラーメン" {
		t.Errorf("ToKatakana(らーめん) = %q, want %q", got, "ラーメン")
	}
	if got := ToHiragana("ｱｲｳｴｵ"); got != "あいうえお" {
		t.Errorf("ToHiragana(ｱｲｳｴｵ) = %q, want %q", got, "あいうえお")
	}
}

func TestMoras(t *testing.T) {
	tests := []struct {
		input string
		want  []string
	}{
		{"きょうと", []string{"きょ", "う", "と"}},
		{"がっこう", []string{"が", "っ", "こ", "う"}},
		{"コーヒー", []string{"コ", "ー", "ヒ", "ー"}},
		{"ほんや", []string{"ほ", "ん", "や"}},
		{"ティーパーティー", []string{"ティ", "ー", "パ", "ー", "ティ", "ー"}},
	}
	for _, tc := range tests {
		got := Moras(tc.input)
		if !reflect.DeepEqual(got, tc.want) {
			t.Errorf("Moras(%q) = %v, want %v", tc.input, got, tc.want)
		}
	}
}

func TestRomaji(t *testing.T) {
	tests := []struct {
		input string
		style Style
		want  string
	}{
		// きょう -> kyou in context-free Romaji
		{"きょう", Macron, "kyou"},
		{"きょう", Wapuro, "kyou"},
		{"きょう", Plain, "kyou"},

		// がっこう
		{"がっこう", Macron, "gakkou"},
		{"がっこう", Wapuro, "gakkou"},

		// まっちゃ -> matcha
		{"まっちゃ", Macron, "matcha"},
		{"まっちゃ", Wapuro, "matcha"},

		// ほんや -> hon'ya
		{"ほんや", Macron, "hon'ya"},
		{"ほんや", Wapuro, "hon'ya"},

		// きんえん -> kin'en
		{"きんえん", Macron, "kin'en"},
		{"きんえん", Wapuro, "kin'en"},

		// しんぶん -> shinbun (not shimbun)
		{"しんぶん", Macron, "shinbun"},
		{"しんぶん", Wapuro, "shinbun"},

		// コーヒー
		{"コーヒー", Macron, "kōhī"},
		{"コーヒー", Wapuro, "koohii"},
		{"コーヒー", Plain, "kohi"},

		// Particle を / ヲ
		{"を", Macron, "o"},
		{"を", Plain, "o"},
		{"を", Wapuro, "wo"},
		{"ヲ", Macron, "o"},
		{"ヲ", Plain, "o"},
		{"ヲ", Wapuro, "wo"},
	}

	for _, tc := range tests {
		got := Romaji(tc.input, tc.style)
		if got != tc.want {
			t.Errorf("Romaji(%q, style=%v) = %q, want %q", tc.input, tc.style, got, tc.want)
		}
	}
}

func TestRomajiPron(t *testing.T) {
	tests := []struct {
		reading string
		pron    string
		style   Style
		want    string
	}{
		// きょう -> kyō only via RomajiPron(キョウ, キョー)
		{"キョウ", "キョー", Macron, "kyō"},
		{"キョウ", "キョー", Wapuro, "kyou"},
		{"キョウ", "キョー", Plain, "kyo"},

		// トウキョウ
		{"トウキョウ", "トーキョー", Macron, "tōkyō"},
		{"トウキョウ", "トーキョー", Wapuro, "toukyou"},
		{"トウキョウ", "トーキョー", Plain, "tokyo"},

		// オモウ
		{"オモウ", "オモウ", Macron, "omou"},
		{"オモウ", "オモウ", Wapuro, "omou"},
		{"オモウ", "オモウ", Plain, "omou"},

		// センセイ
		{"センセイ", "センセー", Macron, "sensei"},
		{"センセイ", "センセー", Wapuro, "sensei"},
		{"センセイ", "センセー", Plain, "sensei"},

		// ハ -> wa
		{"ハ", "ワ", Macron, "wa"},
		{"ハ", "ワ", Wapuro, "wa"},

		// オオキイ
		{"オオキイ", "オーキイ", Macron, "ōkii"},
		{"オオキイ", "オーキイ", Wapuro, "ookii"},
		{"オオキイ", "オーキイ", Plain, "okii"},

		// ヘ -> e
		{"ヘ", "エ", Macron, "e"},
		{"ヘ", "エ", Wapuro, "e"},

		// ヲ -> o (in Macron and Plain, and when pron is オ)
		{"ヲ", "オ", Macron, "o"},
		{"ヲ", "オ", Plain, "o"},
		{"ヲ", "オ", Wapuro, "o"},

		// ヲ with pron ヲ: "o" in Macron & Plain per Hepburn, "wo" in Wapuro per WanaKana
		{"ヲ", "ヲ", Macron, "o"},
		{"ヲ", "ヲ", Plain, "o"},
		{"ヲ", "ヲ", Wapuro, "wo"},
	}

	for _, tc := range tests {
		got := RomajiPron(tc.reading, tc.pron, tc.style)
		if got != tc.want {
			t.Errorf("RomajiPron(%q, %q, style=%v) = %q, want %q", tc.reading, tc.pron, tc.style, got, tc.want)
		}
	}
}

func TestIterationMarks(t *testing.T) {
	if got := Romaji("さゝ", Macron); got != "sasa" {
		t.Errorf("Romaji('さゝ') = %q, want 'sasa'", got)
	}
	if got := Romaji("はゞ", Macron); got != "haba" {
		t.Errorf("Romaji('はゞ') = %q, want 'haba'", got)
	}
	if got := Romaji("サヽ", Macron); got != "sasa" {
		t.Errorf("Romaji('サヽ') = %q, want 'sasa'", got)
	}
	if got := Romaji("ハヾ", Macron); got != "haba" {
		t.Errorf("Romaji('ハヾ') = %q, want 'haba'", got)
	}
}

func TestRomajiFallbacks(t *testing.T) {
	if got := RomajiPron("きょう", "", Macron); got != "kyou" {
		t.Errorf("RomajiPron('きょう', '') = %q, want 'kyou'", got)
	}
	if got := RomajiPron("きょう", "トウキョウ", Macron); got != "kyou" {
		t.Errorf("RomajiPron mismatched = %q, want 'kyou'", got)
	}
}

func TestRomajiPunctuation(t *testing.T) {
	if got := Romaji("こんにちは!", Macron); got != "konnichiha!" {
		t.Errorf("Romaji('こんにちは!') = %q, want 'konnichiha!'", got)
	}
}

//go:embed testdata/wanakana_sample_5k.json.gz
var wanakanaSampleGz []byte

// TestWanaKanaParity verifies 100% parity against real WanaKana over distinct JMdict kana readings.
// By default it tests the embedded 5k representative sample hermetically.
// If testdata/wanakana_pairs.json.gz is present, it tests the complete 193,414 readings.
func TestWanaKanaParity(t *testing.T) {
	var gzReader *gzip.Reader
	var err error

	fullPath := "testdata/wanakana_pairs.json.gz"
	if f, errOpen := os.Open(fullPath); errOpen == nil {
		defer f.Close()
		gzReader, err = gzip.NewReader(f)
	} else {
		gzReader, err = gzip.NewReader(bytes.NewReader(wanakanaSampleGz))
	}
	if err != nil {
		t.Fatalf("failed to create gzip reader: %v", err)
	}
	defer gzReader.Close()

	var pairs [][]string
	if err := json.NewDecoder(gzReader).Decode(&pairs); err != nil {
		t.Fatalf("failed to decode json: %v", err)
	}

	mismatches := 0
	for _, p := range pairs {
		kana := p[0]
		expected := p[1]
		got := Romaji(kana, Wapuro)
		if got != expected {
			mismatches++
			if mismatches <= 10 {
				t.Errorf("WanaKana mismatch for %q: got %q, want %q", kana, got, expected)
			}
		}
	}
	t.Logf("Tested %d distinct kana readings against WanaKana: %d mismatches", len(pairs), mismatches)
	if mismatches != 0 {
		t.Fatalf("expected 0 mismatches, got %d", mismatches)
	}
}
