package sandbox

import (
	"context"
	"strings"
	"testing"
)

// ── ScanSource ──────────────────────────────

func TestScanSource_CleanStarlark(t *testing.T) {
	source := `
def on_request(req):
    path = req["path"]
    method = req["method"]
    return {"status": 200, "body": "ok"}
`
	findings := ScanSource(source, "clean.star")
	if len(findings) != 0 {
		t.Errorf("expected 0 findings for clean source, got %d", len(findings))
	}
}

func TestScanSource_FewVariationSelectors(t *testing.T) {
	// 1-2 variation selectors — legitimate emoji use (e.g. text vs emoji presentation)
	source := "emoji = \"\u2764\uFE0F\"  # heart with variation selector"
	findings := ScanSource(source, "emoji.star")
	if len(findings) != 1 {
		t.Fatalf("expected 1 finding for single VS, got %d", len(findings))
	}
	if findings[0].Category != "variation_selector" {
		t.Errorf("expected category variation_selector, got %q", findings[0].Category)
	}
	if findings[0].Rune != '\uFE0F' {
		t.Errorf("expected rune U+FE0F, got U+%04X", findings[0].Rune)
	}
}

func TestScanSource_GlassWormPattern(t *testing.T) {
	// 50+ variation selectors — GlassWorm payload pattern
	var sb strings.Builder
	sb.WriteString("data = \"")
	for i := 0; i < 50; i++ {
		sb.WriteRune(rune(0xFE00 + (i % 16))) // cycle through VS1-VS16
	}
	sb.WriteString("\"")
	source := sb.String()

	findings := ScanSource(source, "glassworm.star")
	count := 0
	for _, f := range findings {
		if f.Category == "variation_selector" {
			count++
		}
	}
	if count != 50 {
		t.Errorf("expected 50 variation_selector findings, got %d", count)
	}
}

func TestScanSource_BidiOverride(t *testing.T) {
	// RLO (U+202E) — Trojan Source attack
	source := "name = \"\u202Emalicious\""
	findings := ScanSource(source, "bidi.star")
	if len(findings) != 1 {
		t.Fatalf("expected 1 finding, got %d", len(findings))
	}
	if findings[0].Category != "bidi_override" {
		t.Errorf("expected bidi_override, got %q", findings[0].Category)
	}
}

func TestScanSource_BidiIsolate(t *testing.T) {
	// LRI (U+2066), PDI (U+2069)
	source := "text = \"\u2066hidden\u2069\""
	findings := ScanSource(source, "bidi_isolate.star")
	bidiCount := 0
	for _, f := range findings {
		if f.Category == "bidi_override" {
			bidiCount++
		}
	}
	if bidiCount != 2 {
		t.Errorf("expected 2 bidi_override findings, got %d", bidiCount)
	}
}

func TestScanSource_ZeroWidthBelow(t *testing.T) {
	// 3 zero-width chars — below threshold
	source := "a = \"\u200B\u200B\u200B\""
	findings := ScanSource(source, "zw.star")
	zwCount := 0
	for _, f := range findings {
		if f.Category == "zero_width" {
			zwCount++
		}
	}
	if zwCount != 3 {
		t.Errorf("expected 3 zero_width findings, got %d", zwCount)
	}
}

func TestScanSource_ZeroWidthAbove(t *testing.T) {
	// 8 zero-width chars — above threshold
	source := "a = \"\u200B\u200B\u200B\u200B\u200B\u200B\u200B\u200B\""
	findings := ScanSource(source, "zw_many.star")
	zwCount := 0
	for _, f := range findings {
		if f.Category == "zero_width" {
			zwCount++
		}
	}
	if zwCount != 8 {
		t.Errorf("expected 8 zero_width findings, got %d", zwCount)
	}
}

func TestScanSource_TagCharacters(t *testing.T) {
	// Tag characters (U+E0001-U+E007F)
	var sb strings.Builder
	sb.WriteString("tag = \"")
	for i := 0; i < 15; i++ {
		sb.WriteRune(rune(0xE0001 + i))
	}
	sb.WriteString("\"")
	source := sb.String()

	findings := ScanSource(source, "tags.star")
	tagCount := 0
	for _, f := range findings {
		if f.Category == "tag_character" {
			tagCount++
		}
	}
	if tagCount != 15 {
		t.Errorf("expected 15 tag_character findings, got %d", tagCount)
	}
}

func TestScanSource_HangulFiller(t *testing.T) {
	source := "x = \"\u3164\"" // Hangul filler
	findings := ScanSource(source, "hangul.star")
	if len(findings) != 1 {
		t.Fatalf("expected 1 finding, got %d", len(findings))
	}
	if findings[0].Category != "other_invisible" {
		t.Errorf("expected other_invisible, got %q", findings[0].Category)
	}
}

func TestScanSource_LineTracking(t *testing.T) {
	source := "line1\nline2\nline3 = \"\u202E\""
	findings := ScanSource(source, "lines.star")
	if len(findings) != 1 {
		t.Fatalf("expected 1 finding, got %d", len(findings))
	}
	if findings[0].Line != 3 {
		t.Errorf("expected line 3, got %d", findings[0].Line)
	}
}

func TestScanSource_VariationSelectorSupplement(t *testing.T) {
	// U+E0100 — GlassWorm secondary range (R32)
	var sb strings.Builder
	sb.WriteString("x = \"")
	for i := 0; i < 12; i++ {
		sb.WriteRune(rune(0xE0100 + i))
	}
	sb.WriteString("\"")
	source := sb.String()

	findings := ScanSource(source, "vs_supp.star")
	vsCount := 0
	for _, f := range findings {
		if f.Category == "variation_selector" {
			vsCount++
		}
	}
	if vsCount != 12 {
		t.Errorf("expected 12 variation_selector findings from supplement range, got %d", vsCount)
	}
}

// ── Verdict ─────────────────────────────────

func TestVerdict_NoFindings(t *testing.T) {
	blocked, reason := Verdict(nil)
	if blocked {
		t.Error("expected not blocked for nil findings")
	}
	if reason != "" {
		t.Errorf("expected empty reason, got %q", reason)
	}
}

func TestVerdict_BidiAlwaysBlocks(t *testing.T) {
	findings := []Finding{{Rune: 0x202E, Category: "bidi_override"}}
	blocked, reason := Verdict(findings)
	if !blocked {
		t.Error("expected blocked for bidi_override")
	}
	if !strings.Contains(reason, "bidirectional override") {
		t.Errorf("expected bidi reason, got %q", reason)
	}
}

func TestVerdict_GlassWormBlocks(t *testing.T) {
	var findings []Finding
	for i := 0; i < 10; i++ {
		findings = append(findings, Finding{Rune: rune(0xFE00 + i), Category: "variation_selector"})
	}
	blocked, reason := Verdict(findings)
	if !blocked {
		t.Error("expected blocked for 10 variation selectors")
	}
	if !strings.Contains(reason, "GlassWorm") {
		t.Errorf("expected GlassWorm reason, got %q", reason)
	}
}

func TestVerdict_GlassWormMixedVSAndTag(t *testing.T) {
	var findings []Finding
	for i := 0; i < 5; i++ {
		findings = append(findings, Finding{Rune: rune(0xFE00 + i), Category: "variation_selector"})
	}
	for i := 0; i < 5; i++ {
		findings = append(findings, Finding{Rune: rune(0xE0001 + i), Category: "tag_character"})
	}
	blocked, _ := Verdict(findings)
	if !blocked {
		t.Error("expected blocked for 5 VS + 5 tag = 10 total")
	}
}

func TestVerdict_FewVariationSelectorsPass(t *testing.T) {
	findings := []Finding{
		{Rune: 0xFE0F, Category: "variation_selector"},
		{Rune: 0xFE0E, Category: "variation_selector"},
	}
	blocked, _ := Verdict(findings)
	if blocked {
		t.Error("expected not blocked for 2 variation selectors (emoji use)")
	}
}

func TestVerdict_ZeroWidthBelowThresholdPasses(t *testing.T) {
	var findings []Finding
	for i := 0; i < 5; i++ {
		findings = append(findings, Finding{Rune: 0x200B, Category: "zero_width"})
	}
	blocked, _ := Verdict(findings)
	if blocked {
		t.Error("expected not blocked for 5 zero_width chars")
	}
}

func TestVerdict_ZeroWidthAboveThresholdBlocks(t *testing.T) {
	var findings []Finding
	for i := 0; i < 6; i++ {
		findings = append(findings, Finding{Rune: 0x200B, Category: "zero_width"})
	}
	blocked, reason := Verdict(findings)
	if !blocked {
		t.Error("expected blocked for 6 zero_width chars")
	}
	if !strings.Contains(reason, "zero-width") {
		t.Errorf("expected zero-width reason, got %q", reason)
	}
}

func TestVerdict_OtherInvisibleDoesNotBlock(t *testing.T) {
	findings := []Finding{
		{Rune: 0x3164, Category: "other_invisible"},
		{Rune: 0xFFF9, Category: "other_invisible"},
	}
	blocked, _ := Verdict(findings)
	if blocked {
		t.Error("expected not blocked for 2 other_invisible chars")
	}
}

// ── GlassWorm payload simulation ────────────

func TestGlassWormPayloadDetection(t *testing.T) {
	// Simulate a GlassWorm-style payload: encode each byte of a secret string
	// as a variation selector offset (U+FE00 + low nibble, U+E0100 + high nibble).
	// This is the core technique: invisible characters carrying data.
	secret := "eval(malicious_code)"
	var sb strings.Builder
	sb.WriteString("innocent_looking_var = \"")
	for _, b := range []byte(secret) {
		// Encode low nibble as VS1-VS16 (U+FE00-U+FE0F)
		sb.WriteRune(rune(0xFE00 + (int(b) & 0x0F)))
		// Encode high nibble as VS supplement (U+E0100-U+E010F)
		sb.WriteRune(rune(0xE0100 + ((int(b) >> 4) & 0x0F)))
	}
	sb.WriteString("\"")

	source := sb.String()
	findings := ScanSource(source, "glassworm_payload.star")

	// Should have 2 findings per character (low + high nibble)
	expectedFindings := len(secret) * 2
	vsCount := 0
	for _, f := range findings {
		if f.Category == "variation_selector" {
			vsCount++
		}
	}
	if vsCount != expectedFindings {
		t.Errorf("expected %d variation_selector findings, got %d", expectedFindings, vsCount)
	}

	// Verdict should block
	blocked, reason := Verdict(findings)
	if !blocked {
		t.Errorf("GlassWorm payload should be blocked, got: %s", reason)
	}
	if !strings.Contains(reason, "GlassWorm") {
		t.Errorf("expected GlassWorm in reason, got: %s", reason)
	}
}

// ── Execution gate integration ──────────────

func TestExecGateBlocksBidiSource(t *testing.T) {
	sb := New(DefaultConfig())
	// Source with a bidi override character
	source := "x = \"\u202E\""
	_, err := sb.Exec(context.Background(), "bidi.star", source, nil)
	if err == nil {
		t.Fatal("expected error from unicode gate for bidi override")
	}
	if !strings.Contains(err.Error(), "unicode security gate") {
		t.Errorf("expected 'unicode security gate' error, got: %v", err)
	}
}

func TestExecGateAllowsCleanSource(t *testing.T) {
	sb := New(DefaultConfig())
	_, err := sb.Exec(context.Background(), "clean.star", "x = 42", nil)
	if err != nil {
		t.Fatalf("clean source should not be blocked: %v", err)
	}
}

func TestExecGateAllowsFewVS(t *testing.T) {
	sb := New(DefaultConfig())
	source := "emoji = \"\u2764\uFE0F\""
	_, err := sb.Exec(context.Background(), "emoji.star", source, nil)
	if err != nil {
		t.Fatalf("2 VS should not be blocked: %v", err)
	}
}
