package csexp_test

import (
	"bytes"
	"testing"

	"git.codemonkeysoftware.net/b/peachy-go/csexp"
	"git.codemonkeysoftware.net/b/peachy-go/csexp/gen"
	"github.com/shoenig/test"
	"github.com/shoenig/test/must"
	"pgregory.net/rapid"
)

func TestWriteToAndStringEquivalent(t *testing.T) {
	rapid.Check(t, func(t *rapid.T) {
		sexp := gen.Sexp().Draw(t, "sexp")
		stringed := sexp.String()
		var buf bytes.Buffer
		n, err := sexp.WriteTo(&buf)
		must.NoError(t, err)
		written := buf.String()
		must.EqOp(t, written, stringed)
		must.EqOp(t, int64(len(stringed)), n)
	})
}

func TestStringAndParseEqual(t *testing.T) {
	rapid.Check(t, func(t *rapid.T) {
		sexp := gen.Sexp().Draw(t, "sexp")
		str := sexp.String()
		parsed, err := csexp.Parse([]byte(str))
		must.NoError(t, err)
		must.Equal(t, sexp, parsed)
	})
}

func TestParseFails(t *testing.T) {
	t.Run("with extra input", rapid.MakeCheck(func(t *rapid.T) {
		sexp := gen.Sexp().Draw(t, "sexp")
		b := []byte(sexp.String())
		extra := rapid.SliceOfN(rapid.Byte(), 1, -1).Draw(t, "extra")
		b = append(b, extra...)
		parsed, err := csexp.Parse(b)
		test.Error(t, err)
		test.Nil(t, parsed)
	}))
	t.Run("with too little input", rapid.MakeCheck(func(t *rapid.T) {
		sexp := gen.Sexp().Draw(t, "sexp")
		b := []byte(sexp.String())
		missing := rapid.IntRange(1, len(b)).Draw(t, "missing")
		b = b[:len(b)-missing]
		parsed, err := csexp.Parse(b)
		test.Error(t, err)
		test.Nil(t, parsed)
	}))
	t.Run("with malformed atom length", rapid.MakeCheck(func(t *rapid.T) {
		atom := gen.Atom().Draw(t, "atom")
		s := []byte(atom.String())
		changeIdx := rapid.IntRange(0, bytes.IndexByte(s, ':')).Draw(t, "changeIdx")
		change := rapid.ByteRange(1, 255).Draw(t, "change")
		s[changeIdx] = change

		parsed, err := csexp.Parse(s)
		test.Error(t, err)
		test.Nil(t, parsed)
	}))
}

func TestCloneEqual(t *testing.T) {
	rapid.Check(t, func(t *rapid.T) {
		sexp := gen.Sexp().Draw(t, "sexp")
		cloned := sexp.Clone()
		must.Equal(t, sexp, cloned)
	})
}

func MustNotBeEqual(t rapid.TB, s1, s2 csexp.Sexp) {
	e1 := s1.Equal(s2)
	e2 := s2.Equal(s1)
	if e1 == !e2 {
		t.Logf("expected Sexp.Equal to be commutative, but got different results")
	}
	if e1 || e2 {
		t.Fatalf("expected sexps not to be equal\ns1: %q\ns2: %q", string(s1.String()), string(s2.String()))
	}
}

func TestNotEqual(t *testing.T) {
	t.Run("append to atom", rapid.MakeCheck(func(t *rapid.T) {
		a1 := gen.Atom().Draw(t, "a1")

		a2 := a1.Clone().(csexp.Atom)
		a2 = append(a2, 'x')

		MustNotBeEqual(t, a1, a2)
	}))
	t.Run("change atom byte", rapid.MakeCheck(func(t *rapid.T) {
		nonEmpty := func(a csexp.Atom) bool { return len(a) >= 1 }
		a1 := gen.Atom().Filter(nonEmpty).Draw(t, "a1")

		a2 := a1.Clone().(csexp.Atom)
		a2[0] = a2[0] + 1

		MustNotBeEqual(t, a1, a2)
	}))
	t.Run("append to list", rapid.MakeCheck(func(t *rapid.T) {
		l1 := gen.List().Draw(t, "l1")

		extraElement := gen.Sexp().Draw(t, "extraElement")
		l2 := l1.Clone().(csexp.List)
		l2 = append(l2, extraElement)

		MustNotBeEqual(t, l1, l2)
	}))
	t.Run("change list element", rapid.MakeCheck(func(t *rapid.T) {
		nonEmpty := func(l csexp.List) bool { return len(l) >= 1 }
		l1 := gen.List().Filter(nonEmpty).Draw(t, "l1")

		isDifferent := func(s csexp.Sexp) bool { return !s.Equal(l1[0]) }
		newElement := gen.Sexp().Filter(isDifferent).Draw(t, "newElement")
		l2 := l1.Clone().(csexp.List)
		l2[0] = newElement

		MustNotBeEqual(t, l1, l2)
	}))
	t.Run("list and atom", rapid.MakeCheck(func(t *rapid.T) {
		atom := gen.Atom().Draw(t, "atom")
		list := gen.List().Draw(t, "list")
		MustNotBeEqual(t, atom, list)
	}))
}
