package harness

import (
	"strings"
	"testing"
)

func TestRedactSecrets(t *testing.T) {
	cases := []struct {
		in       string
		wantN    int
		mustGone string // 脱敏后不应再出现的子串
	}{
		{"我的 key 是 sk-912cf85b16d04b22bcb95f4576423bfb 别外传", 1, "sk-912cf85b16d04b22bcb95f4576423bfb"},
		{"token: AKIAIOSFODNN7EXAMPLE", 1, "AKIAIOSFODNN7EXAMPLE"},
		{"Authorization: Bearer abcdef0123456789ABCDEF", 1, "abcdef0123456789ABCDEF"},
		{"jwt eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.abcd1234 完事", 1, "eyJhbGciOiJIUzI1NiJ9"},
	}
	for _, c := range cases {
		out, n := RedactSecrets(c.in)
		if n != c.wantN {
			t.Errorf("RedactSecrets(%q) 命中数=%d want %d", c.in, n, c.wantN)
		}
		if strings.Contains(out, c.mustGone) {
			t.Errorf("脱敏后仍含密钥: %q", out)
		}
		if !strings.Contains(out, redactMark) {
			t.Errorf("应含脱敏标记: %q", out)
		}
	}
}

func TestRedactSecrets_Clean(t *testing.T) {
	in := "这是一段正常的回答，介绍杭州西湖的历史与十景。"
	out, n := RedactSecrets(in)
	if n != 0 || out != in {
		t.Errorf("正常文本不应被改动: n=%d out=%q", n, out)
	}
}

// PII：手机号 / 邮箱 / 身份证应被脱敏。
func TestRedactSecrets_PII(t *testing.T) {
	cases := []struct {
		in       string
		mustGone string
	}{
		{"联系我 13812345678 谢谢", "13812345678"},
		{"邮箱 zhang.san@example.com 收", "zhang.san@example.com"},
		{"身份证 11010519900307123X 备案", "11010519900307123X"},
	}
	for _, c := range cases {
		out, n := RedactSecrets(c.in)
		if n != 1 {
			t.Errorf("RedactSecrets(%q) 命中=%d want 1", c.in, n)
		}
		if strings.Contains(out, c.mustGone) {
			t.Errorf("脱敏后仍含 PII: %q", out)
		}
	}
}

// 不应误伤：更长数字串里的合法 11 位子串不该被吞，普通词不该当邮箱。
func TestRedactSecrets_NoFalsePositive(t *testing.T) {
	for _, in := range []string{"订单号 138123456789012", "用户名 a@b（非邮箱）", "数量 12345"} {
		if out, n := RedactSecrets(in); n != 0 {
			t.Errorf("不应脱敏 %q，却命中 %d → %q", in, n, out)
		}
	}
}

// streamAll 把分片喂给 StreamRedactor，拼回消费端看到的完整文本与命中数。
func streamAll(chunks ...string) (string, int) {
	r := NewStreamRedactor()
	var sb strings.Builder
	for _, c := range chunks {
		sb.WriteString(r.Push(c))
	}
	sb.WriteString(r.Flush())
	return sb.String(), r.Hits()
}

// 跨分片：密钥被切成两片，逐片都不单独命中正则，流式脱敏须靠缓冲拼接捕获。
func TestStreamRedactor_CrossChunkSecret(t *testing.T) {
	// "sk-912cf85b" + "16d04b22bcb95f4576423bfb"：第一片仅 8 位 body（<16 不命中），第二片无前缀。
	out, n := streamAll("我的 key 是 sk-912cf85b", "16d04b22bcb95f4576423bfb 别外传")
	if strings.Contains(out, "sk-912cf85b16d04b22bcb95f4576423bfb") {
		t.Errorf("跨分片密钥漏检: %q", out)
	}
	if n != 1 {
		t.Errorf("应脱敏 1 处，got %d (%q)", n, out)
	}
	if !strings.Contains(out, redactMark) {
		t.Errorf("应含脱敏标记: %q", out)
	}
}

// 跨分片 + 逐字符流（最严苛）：JWT 一个字符一片，仍须完整捕获。
func TestStreamRedactor_CharByChar(t *testing.T) {
	secret := "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.abcd1234"
	full := "令牌：" + secret + "，妥善保管"
	chunks := make([]string, 0, len(full))
	for _, r := range full {
		chunks = append(chunks, string(r))
	}
	out, n := streamAll(chunks...)
	if strings.Contains(out, secret) {
		t.Errorf("逐字符流 JWT 漏检: %q", out)
	}
	if n != 1 {
		t.Errorf("应脱敏 1 处，got %d (%q)", n, out)
	}
}

// 回归：密钥 body 跨片，且第一片恰好凑够最短长度(16)。贪婪正则会提前脱敏半截、剩余明文泄漏。
// 切点须在原文上定、勿切断完整匹配 → 整条密钥完整脱敏，无碎片。
func TestStreamRedactor_NoFragmentLeak(t *testing.T) {
	out, n := streamAll("凭据 sk-ABCD1234efgh5678", "ijkl90mnop 完毕")
	if strings.Contains(out, "ijkl90mnop") {
		t.Errorf("密钥碎片泄漏: %q", out)
	}
	if strings.Contains(out, "sk-ABCD1234efgh5678") {
		t.Errorf("密钥头部泄漏: %q", out)
	}
	if n != 1 {
		t.Errorf("应脱敏 1 处，got %d (%q)", n, out)
	}
}

// 完整匹配落在尾窗内（身份证后仅跟少量文字）：切点须整体并入暂留，不可从中切断。
func TestStreamRedactor_MatchInTailWindow(t *testing.T) {
	out, n := streamAll("身份证 11010519900307123X 完")
	if strings.Contains(out, "11010519900307123X") {
		t.Errorf("尾窗内身份证漏检: %q", out)
	}
	if n != 1 {
		t.Errorf("应脱敏 1 处，got %d (%q)", n, out)
	}
}

// 干净文本逐片流入，消费端拼回必须与原文逐字节一致（无乱码、无丢字、无暂留残留）。
func TestStreamRedactor_CleanReconstruct(t *testing.T) {
	full := "杭州西湖十景：苏堤春晓、曲院风荷、平湖秋月、断桥残雪，四季皆宜。"
	chunks := make([]string, 0)
	rs := []rune(full)
	for i := 0; i < len(rs); i += 2 { // 每 2 个中文字一片，制造多字节边界压力
		end := i + 2
		if end > len(rs) {
			end = len(rs)
		}
		chunks = append(chunks, string(rs[i:end]))
	}
	out, n := streamAll(chunks...)
	if out != full {
		t.Errorf("干净文本未原样重建:\n got %q\nwant %q", out, full)
	}
	if n != 0 {
		t.Errorf("干净文本不应脱敏，got %d", n)
	}
}
