package protocol

import (
	"encoding/json"
	"go/ast"
	"go/parser"
	"go/token"
	"os"
	"path/filepath"
	"reflect"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"testing"
	"time"
)

// ---------------------------------------------------------------------------
// 1) JSON round-trip: her payload encode → decode sonrası aynı değeri vermeli
// ---------------------------------------------------------------------------

func roundTrip[T any](t *testing.T, name string, v T) string {
	t.Helper()
	raw, err := json.Marshal(v)
	if err != nil {
		t.Fatalf("%s: Marshal: %v", name, err)
	}
	var back T
	if err := json.Unmarshal(raw, &back); err != nil {
		t.Fatalf("%s: Unmarshal(%s): %v", name, raw, err)
	}
	if !reflect.DeepEqual(v, back) {
		t.Fatalf("%s: round-trip değeri değiştirdi\n önce:  %+v\n sonra: %+v\n json:  %s", name, v, back, raw)
	}
	return string(raw)
}

func TestPayloads_RoundTripAndGoldenJSON(t *testing.T) {
	// Türkçe karakterler, tırnak, satır sonu ve HTML özel karakterleri bozulmadan geçmeli.
	nasty := "Çağrı: \"ığüşöç İ\" <b>&</b>\nikinci satır\t✓ 日本語"

	cases := []struct {
		name   string
		golden string // boş değilse birebir beklenen JSON
		run    func(t *testing.T) string
	}{
		{"TextMessage_tam", `{"text":"selam","model":"qwen3"}`, func(t *testing.T) string {
			return roundTrip(t, "TextMessage", TextMessagePayload{Text: "selam", Model: "qwen3"})
		}},
		{"TextMessage_modelsiz", `{"text":"selam"}`, func(t *testing.T) string {
			return roundTrip(t, "TextMessage", TextMessagePayload{Text: "selam"})
		}},
		{"TextMessage_turkce", "", func(t *testing.T) string {
			return roundTrip(t, "TextMessage", TextMessagePayload{Text: nasty})
		}},
		{"Cancel_bos", `{}`, func(t *testing.T) string { return roundTrip(t, "Cancel", CancelPayload{}) }},
		{"Cancel_sebepli", `{"reason":"user"}`, func(t *testing.T) string {
			return roundTrip(t, "Cancel", CancelPayload{Reason: "user"})
		}},
		{"Thinking", `{"messageId":"msg_1"}`, func(t *testing.T) string {
			return roundTrip(t, "Thinking", ThinkingPayload{MessageID: "msg_1"})
		}},
		{"TextDelta", `{"messageId":"msg_1","delta":"mer"}`, func(t *testing.T) string {
			return roundTrip(t, "TextDelta", TextDeltaPayload{MessageID: "msg_1", Delta: "mer"})
		}},
		{"TextDelta_bosluk_korunur", `{"messageId":"m","delta":" "}`, func(t *testing.T) string {
			// delta bir boşluk olabilir; omitempty YOK, alan düşmemeli.
			return roundTrip(t, "TextDelta", TextDeltaPayload{MessageID: "m", Delta: " "})
		}},
		{"TextFinal_asgari", `{"messageId":"m1","content":"c","status":"complete"}`, func(t *testing.T) string {
			return roundTrip(t, "TextFinal", TextFinalPayload{MessageID: "m1", Content: "c", Status: "complete"})
		}},
		{"TextFinal_tam", `{"messageId":"m1","content":"c","status":"interrupted","model":"q","promptTokens":12,"completionTokens":34,"latencyMs":5678}`, func(t *testing.T) string {
			return roundTrip(t, "TextFinal", TextFinalPayload{MessageID: "m1", Content: "c", Status: "interrupted",
				Model: "q", PromptTokens: 12, CompletionTokens: 34, LatencyMs: 5678})
		}},
		{"TextFinal_bos_icerik_yine_gonderilir", `{"messageId":"m1","content":"","status":"error"}`, func(t *testing.T) string {
			// content/status/messageId zorunludur: boş olsa da JSON'da bulunmalı (istemci bunlara güvenir).
			return roundTrip(t, "TextFinal", TextFinalPayload{MessageID: "m1", Status: "error"})
		}},
		{"State", `{"state":"THINKING"}`, func(t *testing.T) string {
			return roundTrip(t, "State", StatePayload{State: StateThinking})
		}},
		{"Interrupted_bos", `{}`, func(t *testing.T) string { return roundTrip(t, "Interrupted", InterruptedPayload{}) }},
		{"Interrupted_tam", `{"messageId":"m","partial":"yarım","reason":"disconnect"}`, func(t *testing.T) string {
			return roundTrip(t, "Interrupted", InterruptedPayload{MessageID: "m", Partial: "yarım", Reason: "disconnect"})
		}},
		{"ConversationUpdated", `{"title":"Başlık"}`, func(t *testing.T) string {
			return roundTrip(t, "ConversationUpdated", ConversationUpdatedPayload{Title: "Başlık"})
		}},
		{"Error_kalici", `{"code":"bad_request","message":"boş"}`, func(t *testing.T) string {
			return roundTrip(t, "Error", ErrorPayload{Code: ErrCodeBadRequest, Message: "boş"})
		}},
		{"Error_tekrar_denenebilir", `{"code":"busy","message":"meşgul","retryable":true}`, func(t *testing.T) string {
			return roundTrip(t, "Error", ErrorPayload{Code: ErrCodeBusy, Message: "meşgul", Retryable: true})
		}},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got := tc.run(t)
			if tc.golden != "" && got != tc.golden {
				t.Fatalf("JSON biçimi değişti (frontend ile uyumsuz olabilir)\n beklenen: %s\n gelen:    %s", tc.golden, got)
			}
		})
	}
}

func TestInbound_DecodeAndPayload(t *testing.T) {
	const frame = `{"type":"text.message","id":"r1","conversationId":"conv_1","payload":{"text":"selam","model":"m"},"bilinmeyenAlan":123}`
	var in Inbound
	if err := json.Unmarshal([]byte(frame), &in); err != nil {
		t.Fatalf("bilinmeyen alan hatasız yok sayılmalı: %v", err)
	}
	if in.Type != ClientTextMessage || in.ID != "r1" || in.ConversationID != "conv_1" {
		t.Fatalf("zarf alanları: %+v", in)
	}
	if !in.Type.Valid() {
		t.Fatal("text.message geçerli olmalı")
	}
	var p TextMessagePayload
	if err := json.Unmarshal(in.Payload, &p); err != nil {
		t.Fatalf("payload çözülemedi: %v", err)
	}
	if p.Text != "selam" || p.Model != "m" {
		t.Fatalf("payload: %+v", p)
	}
}

func TestInbound_EdgeCases(t *testing.T) {
	t.Run("payload_yok", func(t *testing.T) {
		var in Inbound
		if err := json.Unmarshal([]byte(`{"type":"ping"}`), &in); err != nil {
			t.Fatal(err)
		}
		if len(in.Payload) != 0 {
			t.Fatalf("payload boş kalmalı: %q", in.Payload)
		}
	})
	t.Run("payload_null_panik_vermez", func(t *testing.T) {
		var in Inbound
		if err := json.Unmarshal([]byte(`{"type":"text.message","payload":null}`), &in); err != nil {
			t.Fatal(err)
		}
		var p TextMessagePayload
		if err := json.Unmarshal(in.Payload, &p); err != nil {
			t.Fatalf("null payload struct'a çözülebilmeli: %v", err)
		}
		if p.Text != "" {
			t.Fatalf("boş metin beklenirdi: %+v", p)
		}
	})
	t.Run("bilinmeyen_tur_cozulur_ama_gecersiz", func(t *testing.T) {
		var in Inbound
		if err := json.Unmarshal([]byte(`{"type":"hack.me"}`), &in); err != nil {
			t.Fatal(err)
		}
		if in.Type.Valid() {
			t.Fatal("bilinmeyen tür geçerli sayılmamalı")
		}
	})
	t.Run("bozuk_json", func(t *testing.T) {
		var in Inbound
		for _, bad := range []string{``, `{`, `{"type":`, `[]`, `"ping"`, `{"type":123}`} {
			if err := json.Unmarshal([]byte(bad), &in); err == nil {
				t.Fatalf("%q hata vermeliydi", bad)
			}
		}
	})
	t.Run("yanlis_tipli_payload", func(t *testing.T) {
		var p TextMessagePayload
		if err := json.Unmarshal([]byte(`{"text":42}`), &p); err == nil {
			t.Fatal("sayı text alanına çözülmemeli")
		}
	})
	t.Run("minimal_yazim", func(t *testing.T) {
		raw, _ := json.Marshal(Inbound{Type: ClientPing})
		if string(raw) != `{"type":"ping"}` {
			t.Fatalf("id/conversationId/payload boşken yazılmamalı: %s", raw)
		}
	})
}

func TestOutbound_Shape(t *testing.T) {
	t.Run("minimal_sabit_zaman", func(t *testing.T) {
		o := Outbound{Type: ServerPong, At: time.Date(2026, 10, 2, 12, 0, 0, 0, time.UTC)}
		raw, err := json.Marshal(o)
		if err != nil {
			t.Fatal(err)
		}
		if string(raw) != `{"type":"pong","at":"2026-10-02T12:00:00Z"}` {
			t.Fatalf("beklenmeyen biçim: %s", raw)
		}
	})
	t.Run("NewOutbound_utc_ve_dolu", func(t *testing.T) {
		before := time.Now().Add(-time.Second)
		o := NewOutbound(ServerAssistantThinking, "conv_1", ThinkingPayload{MessageID: "m"})
		if o.At.Location() != time.UTC {
			t.Fatalf("At UTC olmalı: %v", o.At.Location())
		}
		if o.At.Before(before) || o.At.After(time.Now().Add(time.Second)) {
			t.Fatalf("At şimdiye yakın olmalı: %v", o.At)
		}
		if o.ConversationID != "conv_1" || o.ReplyTo != "" {
			t.Fatalf("zarf: %+v", o)
		}
	})
	t.Run("tam_zarf_alan_adlari", func(t *testing.T) {
		o := NewOutbound(ServerAssistantTextDelta, "conv_1", TextDeltaPayload{MessageID: "m", Delta: "x"})
		o.ReplyTo = "r1"
		raw, _ := json.Marshal(o)
		var m map[string]any
		if err := json.Unmarshal(raw, &m); err != nil {
			t.Fatal(err)
		}
		for _, k := range []string{"type", "conversationId", "replyTo", "at", "payload"} {
			if _, ok := m[k]; !ok {
				t.Fatalf("%q anahtarı eksik: %s", k, raw)
			}
		}
		if _, err := time.Parse(time.RFC3339Nano, m["at"].(string)); err != nil {
			t.Fatalf("at RFC3339 değil: %v", err)
		}
	})
	t.Run("payload_tipi_json_sinirinda_kaybolur", func(t *testing.T) {
		// Docs/MIMARI.md'deki uyarının kanıtı: istemci tarafında payload düz nesnedir.
		o := NewOutbound(ServerAssistantTextFinal, "c", TextFinalPayload{MessageID: "m", Content: "x", Status: "complete"})
		raw, _ := json.Marshal(o)
		var back Outbound
		if err := json.Unmarshal(raw, &back); err != nil {
			t.Fatal(err)
		}
		if _, ok := back.Payload.(map[string]any); !ok {
			t.Fatalf("payload map[string]any olmalı, %T geldi", back.Payload)
		}
		if _, isTyped := back.Payload.(TextFinalPayload); isTyped {
			t.Fatal("JSON sonrası tipli payload beklenmez")
		}
	})
}

func TestClientType_ValidCoversAllConstants(t *testing.T) {
	consts := goConsts(t, "ClientType", "")
	if len(consts) == 0 {
		t.Fatal("ClientType sabitleri bulunamadı (ayrıştırıcı bozuk)")
	}
	for _, c := range consts {
		if !ClientType(c).Valid() {
			t.Errorf("%q sabit olarak tanımlı ama Valid() false döndürüyor", c)
		}
	}
	for _, bad := range []string{"", "PING", "text.message ", "assistant.text.delta", "pong"} {
		if ClientType(bad).Valid() {
			t.Errorf("%q geçerli sayılmamalı", bad)
		}
	}
}

func TestNoDuplicateWireNames(t *testing.T) {
	for _, set := range []struct {
		name   string
		values []string
	}{
		{"ClientType", goConsts(t, "ClientType", "")},
		{"ServerType", goConsts(t, "ServerType", "")},
		{"State", goConsts(t, "State", "")},
		{"ErrCode", goConsts(t, "", "ErrCode")},
	} {
		seen := map[string]bool{}
		for _, v := range set.values {
			if v == "" {
				t.Errorf("%s: boş değerli sabit", set.name)
			}
			if seen[v] {
				t.Errorf("%s: %q iki kez tanımlı", set.name, v)
			}
			seen[v] = true
		}
	}
	// İstemci ve sunucu türleri aynı ad uzayında çakışmamalı (ör. "ping" ≠ "pong" ama "error" istemciye ait olamaz).
	client := toSet(goConsts(t, "ClientType", ""))
	for _, s := range goConsts(t, "ServerType", "") {
		if client[s] {
			t.Errorf("%q hem istemci hem sunucu türü", s)
		}
	}
}

// ---------------------------------------------------------------------------
// 2) Go ↔ TypeScript eşlik testi (models/protocol.ts ile birebirlik)
// ---------------------------------------------------------------------------

const tsRelPath = "../../../frontend/src/app/models/protocol.ts"

func readTS(t *testing.T) string {
	t.Helper()
	raw, err := os.ReadFile(filepath.FromSlash(tsRelPath))
	if err != nil {
		if os.IsNotExist(err) {
			t.Skipf("frontend protokol dosyası bulunamadı (%s); backend tek başına test ediliyor", tsRelPath)
		}
		t.Fatal(err)
	}
	return strings.ReplaceAll(string(raw), "\r\n", "\n")
}

// tsUnion `export type NAME = | 'a' | 'b';` birliğinin üyelerini döndürür.
func tsUnion(t *testing.T, src, name string) []string {
	t.Helper()
	re := regexp.MustCompile(`(?s)export type ` + regexp.QuoteMeta(name) + `\s*=(.*?);`)
	m := re.FindStringSubmatch(src)
	if m == nil {
		t.Fatalf("TS birliği bulunamadı: %s", name)
	}
	var out []string
	for _, lit := range regexp.MustCompile(`'([^']+)'`).FindAllStringSubmatch(m[1], -1) {
		out = append(out, lit[1])
	}
	return out
}

// tsInterface arayüz alanlarını döndürür: ad → opsiyonel mi.
func tsInterface(t *testing.T, src, name string) map[string]bool {
	t.Helper()
	re := regexp.MustCompile(`(?s)export interface ` + regexp.QuoteMeta(name) + `\s*\{(.*?)\n\}`)
	m := re.FindStringSubmatch(src)
	if m == nil {
		t.Fatalf("TS arayüzü bulunamadı: %s", name)
	}
	fieldRe := regexp.MustCompile(`^\s*(\w+)(\?)?\s*:`)
	out := map[string]bool{}
	for _, line := range strings.Split(m[1], "\n") {
		trim := strings.TrimSpace(line)
		if strings.HasPrefix(trim, "/**") || strings.HasPrefix(trim, "*") || strings.HasPrefix(trim, "//") {
			continue
		}
		if f := fieldRe.FindStringSubmatch(line); f != nil {
			out[f[1]] = f[2] == "?"
		}
	}
	return out
}

// goConsts protocol.go içindeki string sabitleri döndürür: typeName ile tiplenmiş olanlar
// ve/veya adı namePrefix ile başlayanlar. AST'den okunur, yani yeni sabit eklenince test kendiliğinden kapsar.
func goConsts(t *testing.T, typeName, namePrefix string) []string {
	t.Helper()
	f, err := parser.ParseFile(token.NewFileSet(), "protocol.go", nil, 0)
	if err != nil {
		t.Fatalf("protocol.go ayrıştırılamadı: %v", err)
	}
	var out []string
	for _, d := range f.Decls {
		gd, ok := d.(*ast.GenDecl)
		if !ok || gd.Tok != token.CONST {
			continue
		}
		for _, s := range gd.Specs {
			vs := s.(*ast.ValueSpec)
			match := false
			if id, ok := vs.Type.(*ast.Ident); ok && typeName != "" && id.Name == typeName {
				match = true
			}
			for _, n := range vs.Names {
				if namePrefix != "" && strings.HasPrefix(n.Name, namePrefix) {
					match = true
				}
			}
			if !match {
				continue
			}
			for _, v := range vs.Values {
				if lit, ok := v.(*ast.BasicLit); ok && lit.Kind == token.STRING {
					if s, err := strconv.Unquote(lit.Value); err == nil {
						out = append(out, s)
					}
				}
			}
		}
	}
	return out
}

func toSet(v []string) map[string]bool {
	m := make(map[string]bool, len(v))
	for _, s := range v {
		m[s] = true
	}
	return m
}

func diff(a, b []string) (onlyA, onlyB []string) {
	sa, sb := toSet(a), toSet(b)
	for s := range sa {
		if !sb[s] {
			onlyA = append(onlyA, s)
		}
	}
	for s := range sb {
		if !sa[s] {
			onlyB = append(onlyB, s)
		}
	}
	sort.Strings(onlyA)
	sort.Strings(onlyB)
	return
}

func TestTypeScriptParity_WireNames(t *testing.T) {
	src := readTS(t)
	for _, tc := range []struct {
		name     string
		goVals   []string
		tsUnions string
	}{
		{"ClientType", goConsts(t, "ClientType", ""), "ClientType"},
		{"ServerType", goConsts(t, "ServerType", ""), "ServerType"},
		{"State", goConsts(t, "State", ""), "ConversationState"},
		{"ErrCode", goConsts(t, "", "ErrCode"), "ErrorCode"},
	} {
		t.Run(tc.name, func(t *testing.T) {
			ts := tsUnion(t, src, tc.tsUnions)
			if len(tc.goVals) == 0 || len(ts) == 0 {
				t.Fatalf("boş küme: go=%d ts=%d (ayrıştırma hatası?)", len(tc.goVals), len(ts))
			}
			onlyGo, onlyTS := diff(tc.goVals, ts)
			if len(onlyGo) > 0 || len(onlyTS) > 0 {
				t.Fatalf("protocol.go ↔ protocol.ts uyumsuz\n sadece Go'da: %v\n sadece TS'de: %v", onlyGo, onlyTS)
			}
		})
	}
}

func TestTypeScriptParity_StructFields(t *testing.T) {
	src := readTS(t)
	for _, tc := range []struct {
		goType reflect.Type
		tsName string
	}{
		{reflect.TypeOf(Inbound{}), "Inbound"},
		{reflect.TypeOf(Outbound{}), "Outbound"},
		{reflect.TypeOf(TextMessagePayload{}), "TextMessagePayload"},
		{reflect.TypeOf(CancelPayload{}), "CancelPayload"},
		{reflect.TypeOf(ThinkingPayload{}), "ThinkingPayload"},
		{reflect.TypeOf(TextDeltaPayload{}), "TextDeltaPayload"},
		{reflect.TypeOf(TextFinalPayload{}), "TextFinalPayload"},
		{reflect.TypeOf(StatePayload{}), "StatePayload"},
		{reflect.TypeOf(InterruptedPayload{}), "InterruptedPayload"},
		{reflect.TypeOf(ConversationUpdatedPayload{}), "ConversationUpdatedPayload"},
		{reflect.TypeOf(ErrorPayload{}), "ErrorPayload"},
	} {
		t.Run(tc.tsName, func(t *testing.T) {
			ts := tsInterface(t, src, tc.tsName)
			goFields := map[string]bool{}
			for i := 0; i < tc.goType.NumField(); i++ {
				tag := tc.goType.Field(i).Tag.Get("json")
				if tag == "" || tag == "-" {
					t.Errorf("%s.%s json etiketi yok — alan adı sözleşmede belirsiz", tc.goType.Name(), tc.goType.Field(i).Name)
					continue
				}
				parts := strings.Split(tag, ",")
				optional := false
				for _, p := range parts[1:] {
					if p == "omitempty" {
						optional = true
					}
				}
				goFields[parts[0]] = optional
			}
			for name, goOpt := range goFields {
				tsOpt, ok := ts[name]
				switch {
				case !ok:
					t.Errorf("alan Go'da var, TS'de yok: %q", name)
				case goOpt != tsOpt:
					t.Errorf("%q opsiyonellik uyumsuz: Go omitempty=%v, TS optional=%v", name, goOpt, tsOpt)
				}
			}
			for name := range ts {
				if _, ok := goFields[name]; !ok {
					t.Errorf("alan TS'de var, Go'da yok: %q", name)
				}
			}
		})
	}
}
