package model

import (
	"encoding/json"
	"strings"
	"testing"
)

func TestInboundMarshalJSONNestsObjectFields(t *testing.T) {
	in := Inbound{
		Id:             7,
		Protocol:       VLESS,
		Port:           443,
		Settings:       `{"clients":[],"decryption":"none"}`,
		StreamSettings: `{"network":"tcp"}`,
		Sniffing:       `{"enabled":true}`,
	}
	out, err := json.Marshal(in)
	if err != nil {
		t.Fatalf("Marshal failed: %v", err)
	}
	var parsed map[string]any
	if err := json.Unmarshal(out, &parsed); err != nil {
		t.Fatalf("output is not valid JSON: %v", err)
	}
	for _, field := range []string{"settings", "streamSettings", "sniffing"} {
		if _, ok := parsed[field].(map[string]any); !ok {
			t.Errorf("expected %s to marshal as a JSON object, got %T", field, parsed[field])
		}
	}
	if strings.Contains(string(out), `"settings":"`) {
		t.Errorf("settings should not be emitted as a JSON string: %s", out)
	}
}

func TestInboundMarshalJSONEmptyFieldsBecomeNull(t *testing.T) {
	in := Inbound{Id: 1, Protocol: VLESS}
	out, err := json.Marshal(in)
	if err != nil {
		t.Fatalf("Marshal failed: %v", err)
	}
	var parsed map[string]any
	if err := json.Unmarshal(out, &parsed); err != nil {
		t.Fatalf("output is not valid JSON: %v", err)
	}
	for _, field := range []string{"settings", "streamSettings", "sniffing"} {
		if parsed[field] != nil {
			t.Errorf("expected %s to be null, got %v", field, parsed[field])
		}
	}
}

func TestInboundUnmarshalJSONAcceptsBothShapes(t *testing.T) {
	cases := []struct {
		name string
		body string
	}{
		{
			name: "nested objects (modern)",
			body: `{"id":1,"settings":{"clients":[],"decryption":"none"},"streamSettings":{"network":"tcp"},"sniffing":{"enabled":true}}`,
		},
		{
			name: "JSON-encoded strings (legacy)",
			body: `{"id":1,"settings":"{\"clients\":[],\"decryption\":\"none\"}","streamSettings":"{\"network\":\"tcp\"}","sniffing":"{\"enabled\":true}"}`,
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			var in Inbound
			if err := json.Unmarshal([]byte(tc.body), &in); err != nil {
				t.Fatalf("Unmarshal failed: %v", err)
			}
			if !strings.Contains(in.Settings, `"decryption":"none"`) {
				t.Errorf("Settings not normalised: %q", in.Settings)
			}
			if !strings.Contains(in.StreamSettings, `"network":"tcp"`) {
				t.Errorf("StreamSettings not normalised: %q", in.StreamSettings)
			}
			if !strings.Contains(in.Sniffing, `"enabled":true`) {
				t.Errorf("Sniffing not normalised: %q", in.Sniffing)
			}
		})
	}
}

func TestInboundMarshalJSONInvalidTextFallsBackToString(t *testing.T) {
	in := Inbound{Id: 1, Settings: "not json at all"}
	out, err := json.Marshal(in)
	if err != nil {
		t.Fatalf("Marshal failed: %v", err)
	}
	if !strings.Contains(string(out), `"settings":"not json at all"`) {
		t.Errorf("expected invalid settings text to be wrapped as a JSON string, got %s", out)
	}
}

func TestClientRecordMarshalJSONNestsReverse(t *testing.T) {
	rec := ClientRecord{Id: 1, Email: "alice@example.com", Reverse: `{"tag":"vless-in"}`}
	out, err := json.Marshal(rec)
	if err != nil {
		t.Fatalf("Marshal failed: %v", err)
	}
	var parsed map[string]any
	if err := json.Unmarshal(out, &parsed); err != nil {
		t.Fatalf("output is not valid JSON: %v", err)
	}
	obj, ok := parsed["reverse"].(map[string]any)
	if !ok {
		t.Fatalf("expected reverse to marshal as a JSON object, got %T", parsed["reverse"])
	}
	if obj["tag"] != "vless-in" {
		t.Errorf("expected tag to be preserved, got %v", obj["tag"])
	}
}

func TestClientRecordMarshalJSONEmptyReverseIsNull(t *testing.T) {
	rec := ClientRecord{Id: 1, Email: "alice@example.com"}
	out, err := json.Marshal(rec)
	if err != nil {
		t.Fatalf("Marshal failed: %v", err)
	}
	var parsed map[string]any
	if err := json.Unmarshal(out, &parsed); err != nil {
		t.Fatalf("output is not valid JSON: %v", err)
	}
	if parsed["reverse"] != nil {
		t.Errorf("expected reverse to be null, got %v", parsed["reverse"])
	}
}

func TestClientRecordUnmarshalJSONAcceptsBothShapes(t *testing.T) {
	cases := []struct {
		name string
		body string
	}{
		{name: "nested object", body: `{"id":1,"reverse":{"tag":"vless-in"}}`},
		{name: "legacy string", body: `{"id":1,"reverse":"{\"tag\":\"vless-in\"}"}`},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			var rec ClientRecord
			if err := json.Unmarshal([]byte(tc.body), &rec); err != nil {
				t.Fatalf("Unmarshal failed: %v", err)
			}
			if !strings.Contains(rec.Reverse, `"tag":"vless-in"`) {
				t.Errorf("Reverse not normalised: %q", rec.Reverse)
			}
		})
	}
}

func TestInboundClientIpsMarshalJSONNestsArray(t *testing.T) {
	row := InboundClientIps{Id: 1, ClientEmail: "alice@example.com", Ips: `[{"ip":"1.2.3.4","timestamp":1700000000}]`}
	out, err := json.Marshal(row)
	if err != nil {
		t.Fatalf("Marshal failed: %v", err)
	}
	var parsed map[string]any
	if err := json.Unmarshal(out, &parsed); err != nil {
		t.Fatalf("output is not valid JSON: %v", err)
	}
	arr, ok := parsed["ips"].([]any)
	if !ok {
		t.Fatalf("expected ips to marshal as a JSON array, got %T", parsed["ips"])
	}
	if len(arr) != 1 {
		t.Errorf("expected 1 entry, got %d", len(arr))
	}
}

func TestInboundClientIpsUnmarshalJSONAcceptsBothShapes(t *testing.T) {
	cases := []struct {
		name string
		body string
	}{
		{name: "nested array", body: `{"id":1,"ips":[{"ip":"1.2.3.4","timestamp":1}]}`},
		{name: "legacy string", body: `{"id":1,"ips":"[{\"ip\":\"1.2.3.4\",\"timestamp\":1}]"}`},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			var row InboundClientIps
			if err := json.Unmarshal([]byte(tc.body), &row); err != nil {
				t.Fatalf("Unmarshal failed: %v", err)
			}
			if !strings.Contains(row.Ips, `"ip":"1.2.3.4"`) {
				t.Errorf("Ips not normalised: %q", row.Ips)
			}
		})
	}
}

func TestStripInboundXhttpClientFields_RemovesClientOnlyKnobs(t *testing.T) {
	stream := `{
		"network": "xhttp",
		"security": "reality",
		"xhttpSettings": {
			"path": "/app",
			"host": "example.com",
			"mode": "stream-one",
			"xmux": { "maxConcurrency": "16-32" },
			"downloadSettings": { "network": "xhttp" },
			"scMinPostsIntervalMs": "20-40",
			"uplinkChunkSize": 4096,
			"noGRPCHeader": true
		}
	}`
	out, changed := StripInboundXhttpClientFields(stream)
	if !changed {
		t.Fatal("expected client-only xhttp fields to be stripped")
	}
	if strings.Contains(out, `"xmux"`) {
		t.Fatalf("xmux should be removed from xray config stream: %s", out)
	}
	for _, key := range []string{"downloadSettings", "scMinPostsIntervalMs", "uplinkChunkSize", "noGRPCHeader"} {
		if strings.Contains(out, `"`+key+`"`) {
			t.Fatalf("%s should be removed from xray config stream: %s", key, out)
		}
	}
	var parsed map[string]any
	if err := json.Unmarshal([]byte(out), &parsed); err != nil {
		t.Fatalf("invalid JSON: %v", err)
	}
	xhttp := parsed["xhttpSettings"].(map[string]any)
	if xhttp["path"] != "/app" || xhttp["host"] != "example.com" {
		t.Fatalf("server fields must survive: %#v", xhttp)
	}
}

func TestStripInboundXhttpClientFields_UnchangedWithoutClientFields(t *testing.T) {
	stream := `{"network":"xhttp","xhttpSettings":{"path":"/app","mode":"stream-one"}}`
	out, changed := StripInboundXhttpClientFields(stream)
	if changed {
		t.Fatalf("expected no change, got: %s", out)
	}
	if out != stream {
		t.Fatalf("unchanged stream must be returned verbatim")
	}
}

func TestStripInboundXhttpClientFields_NonXhttpPassthrough(t *testing.T) {
	stream := `{"network":"ws","wsSettings":{"path":"/"}}`
	out, changed := StripInboundXhttpClientFields(stream)
	if changed || out != stream {
		t.Fatalf("non-xhttp stream must pass through unchanged, got changed=%v out=%s", changed, out)
	}
}

func TestGenXrayInboundConfig_OmitsInboundXmuxButDbRowUnchanged(t *testing.T) {
	stream := `{
		"network": "xhttp",
		"xhttpSettings": {
			"path": "/app",
			"mode": "stream-one",
			"xmux": { "maxConcurrency": "16-32", "hMaxRequestTimes": "600-900" }
		}
	}`
	in := Inbound{
		Protocol:       VLESS,
		Port:           443,
		Listen:         "0.0.0.0",
		Tag:            "in-xhttp",
		Settings:       `{"clients":[],"decryption":"none"}`,
		StreamSettings: stream,
	}
	cfg := in.GenXrayInboundConfig()
	if strings.Contains(string(cfg.StreamSettings), `"xmux"`) {
		t.Fatalf("GenXrayInboundConfig must not emit xmux: %s", cfg.StreamSettings)
	}
	if strings.Contains(in.StreamSettings, `"xmux"`) == false {
		t.Fatal("inbound row streamSettings must still carry xmux for subscriptions")
	}
}
