package main

import (
	"bufio"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"testing"
	"time"

	"fikoyt-local-bridge/eklenti_protokol"
)

// ekYaz: yardimci surecten cerceveli cevap yazar.
func ekYaz(w io.Writer, id json.RawMessage, sonuc string) {
	c, err := protokol.Cerceve(protokol.Mesaj{Id: id, Result: json.RawMessage(sonuc)})
	if err != nil {
		return
	}
	_, _ = w.Write(c)
}

// TestEkYardimci: oturum testlerinin alt sureci. Yalnizca FIKOYT_EK_YARDIMCI=1 ise calisir.
// FIKOYT_EK_MOD=sessiz ise ping'e cevap vermez.
func TestEkYardimci(t *testing.T) {
	if os.Getenv("FIKOYT_EK_YARDIMCI") != "1" {
		return
	}
	mod := os.Getenv("FIKOYT_EK_MOD")
	r := bufio.NewReader(os.Stdin)
	w := os.Stdout
	ayarSayisi := 0
	for {
		m, err := protokol.Oku(r)
		if err != nil {
			os.Exit(0)
		}
		switch m.Method {
		case protokol.YontemMerhaba:
			ekYaz(w, m.Id, `{"protokol":2,"eklenti":"test"}`)
		case protokol.YontemPing:
			if mod == "sessiz" {
				continue
			}
			ekYaz(w, m.Id, `{}`)
		case protokol.YontemAyar:
			ayarSayisi++
		case "ayar-sayisi":
			ekYaz(w, m.Id, fmt.Sprintf(`{"sayi":%d}`, ayarSayisi))
		case "ara":
			for n, parca := range []string{`{"sonuc":"`, `dosya`, `"}`} {
				b, _ := json.Marshal(map[string]any{"id": "p1", "sira": n, "toplam": 3, "veri": parca})
				c, _ := protokol.Cerceve(protokol.Mesaj{Method: protokol.YontemParca, Params: b})
				_, _ = w.Write(c)
			}
			ekYaz(w, m.Id, `{"parcaId":"p1"}`)
		case "olay-gonder":
			// surekli eklenti davranisi: islemden once bir fikoyt/olay bildirimi yollar.
			b, _ := json.Marshal(map[string]any{"tip": "durum", "suruculer": []map[string]string{{"harf": "E", "etiket": "Bellek"}}})
			c, _ := protokol.Cerceve(protokol.Mesaj{Method: protokol.YontemOlay, Params: b})
			_, _ = w.Write(c)
			ekYaz(w, m.Id, `{"tamam":true}`)
		case "uyu":
			// Yavas eklenti: istegi 5 sn geciktirir (zaman asimi ve izolasyon testleri icin).
			time.Sleep(5 * time.Second)
			ekYaz(w, m.Id, `{}`)
		case "buyuk":
			// 768 KB'lik sonuc, 256 KB parcalar halinde.
			ham := `{"veri":"` + strings.Repeat("a", 768*1024-len(`{"veri":""}`)) + `"}`
			const parcaBoyut = 256 * 1024
			toplam := (len(ham) + parcaBoyut - 1) / parcaBoyut
			for n := 0; n < toplam; n++ {
				bas, son := n*parcaBoyut, (n+1)*parcaBoyut
				if son > len(ham) {
					son = len(ham)
				}
				b, _ := json.Marshal(map[string]any{"id": "b1", "sira": n, "toplam": toplam, "veri": ham[bas:son]})
				c, _ := protokol.Cerceve(protokol.Mesaj{Method: protokol.YontemParca, Params: b})
				_, _ = w.Write(c)
			}
			ekYaz(w, m.Id, `{"parcaId":"b1"}`)
		default:
			if len(m.Id) > 0 {
				ekYaz(w, m.Id, `{}`)
			}
		}
	}
}

func ekYardimciArgs() []string {
	return []string{"-test.run=TestEkYardimci", "-test.count=1"}
}

// ekTemizle: testte acilan tum oturumlari kapatir.
func ekTemizle(t *testing.T) {
	t.Helper()
	t.Cleanup(func() {
		ekMu.Lock()
		liste := make([]*ekOturum, 0, len(ekOturumlar))
		for _, o := range ekOturumlar {
			liste = append(liste, o)
		}
		ekMu.Unlock()
		for _, o := range liste {
			o.istemci.Kapat()
		}
	})
}

// Parcalar bridge'de birlestirilmez: her parca sunucuya iletilir, sonuc yalnizca parcaId tasir.
func TestEkCagirParcaIletir(t *testing.T) {
	t.Setenv("FIKOYT_EK_YARDIMCI", "1")
	t.Setenv("FIKOYT_EK_MOD", "normal")
	ekTemizle(t)

	var mu sync.Mutex
	var iletilen []string
	eski := eklentiParcaGonder
	eklentiParcaGonder = func(params json.RawMessage) {
		mu.Lock()
		defer mu.Unlock()
		iletilen = append(iletilen, string(params))
	}
	t.Cleanup(func() { eklentiParcaGonder = eski })

	ctx, iptal := context.WithTimeout(context.Background(), 10*time.Second)
	defer iptal()
	sonuc, err := ekCagir(ctx, "test-parca", os.Args[0], ekYardimciArgs(), "ara", nil, nil)
	if err != nil {
		t.Fatalf("cagri basarisiz: %v", err)
	}
	if string(sonuc) != `{"parcaId":"p1"}` {
		t.Fatalf("sonuc parcaId tasimaliydi: %s", sonuc)
	}
	mu.Lock()
	defer mu.Unlock()
	if len(iletilen) != 3 {
		t.Fatalf("3 parca iletilmeliydi, iletilen: %d", len(iletilen))
	}
	for n, p := range iletilen {
		var m struct {
			Id     string `json:"id"`
			Sira   int    `json:"sira"`
			Toplam int    `json:"toplam"`
		}
		if json.Unmarshal([]byte(p), &m) != nil || m.Id != "p1" || m.Sira != n || m.Toplam != 3 {
			t.Fatalf("parca %d yanlis iletildi: %s", n, p)
		}
	}
}

// 768 KB sonuc: bridge parcalari sirayla iletir; birlestirilince tam ve gecerli JSON olmali.
func TestEkBuyukSonucParcaIletir(t *testing.T) {
	t.Setenv("FIKOYT_EK_YARDIMCI", "1")
	t.Setenv("FIKOYT_EK_MOD", "normal")
	ekTemizle(t)

	var mu sync.Mutex
	var iletilen []json.RawMessage
	eski := eklentiParcaGonder
	eklentiParcaGonder = func(p json.RawMessage) {
		mu.Lock()
		defer mu.Unlock()
		iletilen = append(iletilen, append(json.RawMessage(nil), p...))
	}
	t.Cleanup(func() { eklentiParcaGonder = eski })

	ctx, iptal := context.WithTimeout(context.Background(), 10*time.Second)
	defer iptal()
	sonuc, err := ekCagir(ctx, "test-buyuk", os.Args[0], ekYardimciArgs(), "buyuk", nil, nil)
	if err != nil {
		t.Fatalf("cagri basarisiz: %v", err)
	}
	if string(sonuc) != `{"parcaId":"b1"}` {
		t.Fatalf("sonuc parcaId tasimaliydi: %s", sonuc)
	}

	mu.Lock()
	defer mu.Unlock()
	if len(iletilen) != 3 {
		t.Fatalf("3 parca bekleniyordu, iletilen: %d", len(iletilen))
	}
	var birlesik []byte
	for n, p := range iletilen {
		var m struct {
			Sira int    `json:"sira"`
			Veri string `json:"veri"`
		}
		if json.Unmarshal(p, &m) != nil || m.Sira != n || len(m.Veri) > 256*1024 {
			t.Fatalf("parca %d hatali: sira=%d boyut=%d", n, m.Sira, len(m.Veri))
		}
		birlesik = append(birlesik, m.Veri...)
	}
	if len(birlesik) != 768*1024 {
		t.Fatalf("birlesik boyut 768 KB olmaliydi: %d", len(birlesik))
	}
	if !json.Valid(birlesik) {
		t.Fatal("birlesik sonuc gecerli JSON degil")
	}
}

func TestEkAyarYalnizcaDegisince(t *testing.T) {
	t.Setenv("FIKOYT_EK_YARDIMCI", "1")
	t.Setenv("FIKOYT_EK_MOD", "normal")
	ekTemizle(t)

	ctx, iptal := context.WithTimeout(context.Background(), 10*time.Second)
	defer iptal()
	a := json.RawMessage(`{"kullanici":"a"}`)
	b := json.RawMessage(`{"kullanici":"b"}`)
	for _, ayar := range []json.RawMessage{a, a, b, b, b} {
		if _, err := ekCagir(ctx, "test-ayar", os.Args[0], ekYardimciArgs(), "ping", nil, ayar); err != nil {
			t.Fatalf("cagri basarisiz: %v", err)
		}
	}
	sonuc, err := ekCagir(ctx, "test-ayar", os.Args[0], ekYardimciArgs(), "ayar-sayisi", nil, b)
	if err != nil {
		t.Fatalf("sayi sorgusu basarisiz: %v", err)
	}
	// a ilk kez, b ikinci kez: iki gonderim beklenir.
	if string(sonuc) != `{"sayi":2}` {
		t.Fatalf("ayar yalnizca degisince gonderilmeliydi, sonuc: %s", sonuc)
	}
}

func TestEkPingYanitVermezseYenidenBaslar(t *testing.T) {
	t.Setenv("FIKOYT_EK_YARDIMCI", "1")
	t.Setenv("FIKOYT_EK_MOD", "sessiz")
	ekTemizle(t)

	eskiAralik, eskiZaman, eskiSinir := ekPingAraligi, ekPingZamanAsimi, ekPingHataSiniri
	ekPingAraligi, ekPingZamanAsimi, ekPingHataSiniri = 30*time.Millisecond, 30*time.Millisecond, 2
	t.Cleanup(func() {
		ekPingAraligi, ekPingZamanAsimi, ekPingHataSiniri = eskiAralik, eskiZaman, eskiSinir
	})

	ilk, err := ekOturumAl("test-ping", os.Args[0], ekYardimciArgs())
	if err != nil {
		t.Fatalf("oturum acilamadi: %v", err)
	}
	select {
	case <-ilk.kapali:
	case <-time.After(10 * time.Second):
		t.Fatal("ping cevapsiz kalinca oturum kapanmadi")
	}
	ikinci, err := ekOturumAl("test-ping", os.Args[0], ekYardimciArgs())
	if err != nil {
		t.Fatalf("yeniden baslatma basarisiz: %v", err)
	}
	if ikinci == ilk {
		t.Fatal("kapanan oturum yeniden kullanildi")
	}
}

func TestEkOnayMetniYoksaReddedilir(t *testing.T) {
	if _, ok := ekOnayMetniBul("everything", "bilinmeyen-tur"); ok {
		t.Fatal("tabloda olmayan izin turu kabul edildi")
	}
}

// Bir eklenti baska eklentinin iznini isteyemez; kendi turu ise bulunur.
func TestEkOnayIzolasyonu(t *testing.T) {
	eski := ekOnayMetinleri
	t.Cleanup(func() { ekOnayMetinleri = eski })
	ekOnayMetinleri = map[string]map[string]ekOnayMetni{
		"ornek": {"demo": {Baslik: "Demo", Aciklama: "Demo izni"}},
	}
	if _, ok := ekOnayMetniBul("ornek", "demo"); !ok {
		t.Fatal("eklenting kendi turu bulunamadi")
	}
	if _, ok := ekOnayMetniBul("everything", "demo"); ok {
		t.Fatal("baska eklentinin turu kabul edildi")
	}
}

// Oturumla baslayan eklenti: ekSurecBaslat protokol 2 oturumu acar (ekEskiMi false),
// ekSurecCalisiyor oturumu gorur, ekSurecDurdur oturumu kapatir. surecYonetici'ye dusulmez.
// (ekSurecBaslat'in uretim cagrisi args'siz surec actigi icin dogrudan sinanmaz; burada
// ayni ekOturumAl cagrisiyla acilan oturum uzerinden ekSurecCalisiyor/ekSurecDurdur dogrulanir.)
func TestEkSurecOturumlaBaslar(t *testing.T) {
	t.Setenv("FIKOYT_EK_YARDIMCI", "1")
	t.Setenv("FIKOYT_EK_MOD", "normal")
	ekTemizle(t)

	// ekEskiMi(yol) false (isaretli degil) ve ekOturumAl basarili olursa oturum acilir.
	// Yardimci args'siz calisirsa merhaba'ya cevap vermez; bu yuzden dogrudan ekOturumAl ile
	// oturumu acip ekSurecCalisiyor/ekSurecDurdur'in onu gordugunu test ediyoruz.
	o, err := ekOturumAl("test-oturum", os.Args[0], ekYardimciArgs())
	if err != nil {
		t.Fatalf("oturum acilamadi: %v", err)
	}
	if !ekSurecCalisiyor("test-oturum") {
		t.Fatal("acik oturum calisiyor sayilmadi")
	}
	if err := ekSurecDurdur("test-oturum"); err != nil {
		t.Fatalf("ekSurecDurdur basarisiz: %v", err)
	}
	select {
	case <-o.kapali:
	case <-time.After(5 * time.Second):
		t.Fatal("ekSurecDurdur oturumu kapatmadi")
	}
	if ekSurecCalisiyor("test-oturum") {
		t.Fatal("kapatilan oturum hala calisiyor sayiliyor")
	}
}

// Gercek usb-sdk.exe ile protokol 2 el sikismasi: bridge ekOturumAl ile oturum acar,
// merhaba'ya dogru cevap gelir ve tara islemi calisir. Exe derlenmemisse test atlanir.
func TestUsbSdkElSikisma(t *testing.T) {
	yol := filepath.Join("..", "eklentiler", "usb-sdk", "fikoyt-eklenti-usb.yeni.exe")
	gomutlak, err := filepath.Abs(yol)
	if err != nil {
		t.Skipf("usb-sdk yolu cozulemedi: %v", err)
	}
	if _, err := os.Stat(gomutlak); err != nil {
		t.Skipf("usb-sdk.exe derlenmemis (%s): once usb-sdk go build calistirilmali", gomutlak)
	}
	ekTemizle(t)

	ctx, iptal := context.WithTimeout(context.Background(), 10*time.Second)
	defer iptal()
	o, err := ekOturumAl("usb-sdk", gomutlak, nil)
	if err != nil {
		t.Fatalf("usb-sdk protokol 2 el sikismasi basarisiz: %v", err)
	}
	// Oturum gercekten acik mi ve tara islemi sonuc donuyor mu.
	sonuc, err := o.Cagir(ctx, "tara", nil, nil)
	if err != nil {
		t.Fatalf("usb-sdk tara islemi basarisiz: %v", err)
	}
	var r struct {
		Tamam bool `json:"tamam"`
	}
	if json.Unmarshal(sonuc, &r) != nil || !r.Tamam {
		t.Fatalf("tara sonucu beklenen bicimde degil: %s", sonuc)
	}
}

// Gercek dosya-yoneticisi-sdk.exe ile protokol 2 el sikismasi: bridge ekOturumAl ile oturum
// acar, listele islemi calisir (bos gecici klasor -> kayitlar bos). Exe derlenmemisse atlanir.
func TestDosyaYoneticisiSdkElSikisma(t *testing.T) {
	yol := filepath.Join("..", "eklentiler", "dosya-yoneticisi-sdk", "fikoyt-eklenti-dosya-yoneticisi.yeni.exe")
	gomutlak, err := filepath.Abs(yol)
	if err != nil {
		t.Skipf("dosya-yoneticisi yolu cozulemedi: %v", err)
	}
	if _, err := os.Stat(gomutlak); err != nil {
		t.Skipf("dosya-yoneticisi.yeni.exe derlenmemis (%s): once dosya-yoneticisi-sdk go build calistirilmali", gomutlak)
	}
	ekTemizle(t)

	ctx, iptal := context.WithTimeout(context.Background(), 10*time.Second)
	defer iptal()
	o, err := ekOturumAl("dosya-yoneticisi", gomutlak, nil)
	if err != nil {
		t.Fatalf("dosya-yoneticisi protokol 2 el sikismasi basarisiz: %v", err)
	}
	// listele islemi: bos gecici klasor -> kayitlar bos donmeli (tamam).
	veri, _ := json.Marshal(map[string]string{"yol": t.TempDir()})
	sonuc, err := o.Cagir(ctx, "listele", veri, nil)
	if err != nil {
		t.Fatalf("listele islemi basarisiz: %v", err)
	}
	var r struct {
		Kayitlar []any `json:"kayitlar"`
	}
	if json.Unmarshal(sonuc, &r) != nil {
		t.Fatalf("listele sonucu beklenen bicimde degil: %s", sonuc)
	}
}

// Gercek everything-sdk.exe ile protokol 2 el sikismasi: bridge ekOturumAl ile oturum acar,
// merhaba'ya dogru cevap gelir. ara Everything HTTP sunucusu gerektirdigi icin burada cagrilmaz.
// Exe derlenmemisse test atlanir.
func TestEverythingSdkElSikisma(t *testing.T) {
	yol := filepath.Join("..", "eklentiler", "everything-sdk", "everything-sdk.yeni.exe")
	gomutlak, err := filepath.Abs(yol)
	if err != nil {
		t.Skipf("everything-sdk yolu cozulemedi: %v", err)
	}
	if _, err := os.Stat(gomutlak); err != nil {
		t.Skipf("everything-sdk.yeni.exe derlenmemis (%s): once everything-sdk go build calistirilmali", gomutlak)
	}
	ekTemizle(t)

	// ekOturumAl el sikismayi (merhaba) kendi icinde yapar; context almaz.
	if _, err := ekOturumAl("everything", gomutlak, nil); err != nil {
		t.Fatalf("everything protokol 2 el sikismasi basarisiz: %v", err)
	}
}

// Surekli calisan eklentinin fikoyt/olay bildirimi bridge'den sunucuya aktarilir.
func TestEkSurecOlayIletilir(t *testing.T) {
	t.Setenv("FIKOYT_EK_YARDIMCI", "1")
	t.Setenv("FIKOYT_EK_MOD", "normal")
	ekTemizle(t)

	var mu sync.Mutex
	var iletilen []string
	eski := eklentiOlayIlet
	eklentiOlayIlet = func(anahtar string, satir []byte) {
		mu.Lock()
		defer mu.Unlock()
		iletilen = append(iletilen, string(satir))
	}
	t.Cleanup(func() { eklentiOlayIlet = eski })

	ctx, iptal := context.WithTimeout(context.Background(), 10*time.Second)
	defer iptal()
	if _, err := ekCagir(ctx, "test-olay", os.Args[0], ekYardimciArgs(), "olay-gonder", nil, nil); err != nil {
		t.Fatalf("cagri basarisiz: %v", err)
	}

	// Olay bildirimi cevaptan once yazilir; okuyucu goroutine'i sirasiyla isler. Kisa bekle.
	beklendi := 0
	for i := 0; i < 400 && beklendi < 1; i++ {
		mu.Lock()
		beklendi = len(iletilen)
		mu.Unlock()
		if beklendi < 1 {
			time.Sleep(5 * time.Millisecond)
		}
	}
	mu.Lock()
	defer mu.Unlock()
	if len(iletilen) != 1 {
		t.Fatalf("1 olay bildirimi bekleniyordu, iletilen: %d", len(iletilen))
	}
	var m struct {
		Tip       string `json:"tip"`
		Suruculer []struct {
			Harf   string `json:"harf"`
			Etiket string `json:"etiket"`
		} `json:"suruculer"`
	}
	if json.Unmarshal([]byte(iletilen[0]), &m) != nil {
		t.Fatalf("olay JSON degil: %s", iletilen[0])
	}
	if m.Tip != "durum" || len(m.Suruculer) != 1 || m.Suruculer[0].Harf != "E" {
		t.Fatalf("olay verisi beklenen bicimde degil: %+v", m)
	}
}
