package knotfeed

import (
	"bytes"
	"encoding/hex"
	"slices"
	"strings"
	"testing"

	"github.com/bluesky-social/indigo/atproto/syntax"
	cbg "github.com/whyrusleeping/cbor-gen"
)

const frozenPushedHex = "a4637368617828616261626162616261626162616261626162616261626162616261626162616261626162616261626524747970657273682e74616e676c65642e6769742e7265666d782d746e676c2d656469746f726e6469643a706c633a6c696d70657473782d746e676c2d707573682d6f7074696f6e738167736b69702d6369"

const frozenBareHex = "a2637368617840636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463646524747970657273682e74616e676c65642e6769742e726566"

func TestFrozenRecordBytesDecodeAndEncodeBackAsThemselves(t *testing.T) {
	for _, tt := range []struct {
		name string
		hex  string
		want RefRecord
	}{
		{"pushed", frozenPushedHex, RefRecord{
			Sha: sha(t, strings.Repeat("ab", 20)), Editor: "did:plc:limpet", PushOptions: []string{"skip-ci"},
		}},
		{"catch-up", frozenBareHex, RefRecord{Sha: sha(t, strings.Repeat("cd", 32))}},
	} {
		t.Run(tt.name, func(t *testing.T) {
			data, err := hex.DecodeString(tt.hex)
			if err != nil {
				t.Fatalf("frozen hex: %v", err)
			}
			rec, err := DecodeRefRecord(data)
			if err != nil {
				t.Fatalf("DecodeRefRecord: %v", err)
			}
			if rec.Sha != tt.want.Sha || rec.Editor != tt.want.Editor ||
				!slices.Equal(rec.PushOptions, tt.want.PushOptions) {
				t.Fatalf("record = %+v, want %+v", rec, tt.want)
			}
			again, err := encodeRefRecord(tt.want)
			if err != nil {
				t.Fatalf("encodeRefRecord: %v", err)
			}
			if !bytes.Equal(again, data) {
				t.Errorf("re-encoded record\n got %x\nwant %x", again, data)
			}
		})
	}
}

func shaRecord(t *testing.T, reported string) []byte {
	t.Helper()
	var out bytes.Buffer
	if err := cbg.CborWriteHeader(&out, cbg.MajMap, 1); err != nil {
		t.Fatalf("map header: %v", err)
	}
	writeText(&out, "sha")
	writeText(&out, reported)
	return out.Bytes()
}

func TestDecodeRefRecordRefusesMalformedRecords(t *testing.T) {
	const overlongShaRun = "a2637368617840636463646364636463646364636463646364636463646364636463" +
		"64636463646364636463646364636463646364636463646364636463646364636463646364636463" +
		"646524747970657273682e74616e676c65642e6769742e726566"

	overlong, err := hex.DecodeString(overlongShaRun)
	if err != nil {
		t.Fatalf("decoding overlong record: %v", err)
	}
	var hugeArray bytes.Buffer
	if err := cbg.CborWriteHeader(&hugeArray, cbg.MajMap, 1); err != nil {
		t.Fatalf("map header: %v", err)
	}
	writeText(&hugeArray, "x-tngl-push-options")
	if err := cbg.CborWriteHeader(&hugeArray, cbg.MajArray, 1<<32); err != nil {
		t.Fatalf("array header: %v", err)
	}

	refused := [][]byte{{0xa1, 0x63, 'a', 'b', 0x00}, hugeArray.Bytes(), overlong}
	for _, reported := range []string{"", "def456", strings.Repeat("ab", 21), strings.Repeat("zz", 20)} {
		refused = append(refused, shaRecord(t, reported))
	}
	for _, data := range refused {
		if _, err := DecodeRefRecord(data); err == nil {
			t.Errorf("DecodeRefRecord accepted % x", data)
		}
	}

	for _, reported := range []string{strings.Repeat("ab", 20), strings.Repeat("AB", 32)} {
		if _, err := DecodeRefRecord(shaRecord(t, reported)); err != nil {
			t.Errorf("DecodeRefRecord(%q): %v", reported, err)
		}
	}
}

func TestEncodeRefRecordRefusesWhatItsOwnDecoderWouldReject(t *testing.T) {
	present := sha(t, strings.Repeat("ab", 20))
	huge := strings.Repeat("x", cbg.MaxLength+1)
	for _, tt := range []struct {
		name string
		rec  RefRecord
	}{
		{"no sha at all", RefRecord{}},
		{"editor past the cbor limit", RefRecord{Sha: present, Editor: syntax.DID(huge)}},
		{"push option past the cbor limit", RefRecord{Sha: present, PushOptions: []string{huge}}},
	} {
		if _, err := encodeRefRecord(tt.rec); err == nil {
			t.Errorf("%s encoded anyway, and the record would fail its own decode", tt.name)
		}
	}
}

func TestEscapeRefnameRulesTheRkey(t *testing.T) {
	for _, tt := range []struct {
		refname string
		want    syntax.RecordKey
		wantOK  bool
	}{
		{"refs/heads/main", "refs~2fheads~2fmain", true},
		{"refs/heads/~x", "refs~2fheads~2f~7ex", true},
		{strings.Repeat("/", 79), syntax.RecordKey(strings.Repeat("~2f", 79)), true},
		{strings.Repeat("/", 80), "", false},
	} {
		rkey, ok := EscapeRefname(tt.refname)
		if ok != tt.wantOK || (ok && rkey != tt.want) {
			t.Fatalf("EscapeRefname(%q) = %q, %v", tt.refname, rkey, ok)
		}
	}
}

func TestUnescapeRkeyRoundTripsPlainAndEscapedRefnames(t *testing.T) {
	for _, refname := range []string{
		"refs/heads/main",
		"main",
		"refs/tags/v1.0.0+build",
		"refs/heads/feature/foo_bar-squid.conch",
		"refs/heads/~tilde",
		"refs/heads/ branch with spaces ",
		"refs/heads/日本語ブランチ",
		"refs/heads/emoji-🍀",
	} {
		rkey, ok := EscapeRefname(refname)
		if !ok {
			t.Fatalf("EscapeRefname refused %q", refname)
		}
		decoded, ok := UnescapeRkey(rkey)
		if !ok {
			t.Fatalf("UnescapeRkey refused %q", rkey)
		}
		if decoded != refname {
			t.Fatalf("round trip of %q answered %q", refname, decoded)
		}
	}
}

func TestUnescapeRkeyRefusesMalformedKeys(t *testing.T) {
	for _, rkey := range []syntax.RecordKey{
		"refs~2fheads~",
		"refs~2fheads~2",
		"refs~2fheads~zz",
		"refs~2fhea ds",
		"refs~ff",
	} {
		if decoded, ok := UnescapeRkey(rkey); ok {
			t.Fatalf("UnescapeRkey(%q) = %q, want refusal", rkey, decoded)
		}
	}
}
