// Copyright 2015 The Go Authors.  All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package binres

import (
	"bytes"
	"encoding"
	"encoding/xml"
	"errors"
	"fmt"
	"math"
	"os"
	"sort"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"
)

func init() {
	skipSynthesize = true
}

func printrecurse(t *testing.T, pl *Pool, el *Element, ws string) {
	t.Logf("%s+elem:ns(%v) name(%s)", ws, el.NS, el.Name.Resolve(pl))

	for _, attr := range el.attrs {
		ns := ""
		if attr.NS != math.MaxUint32 {
			ns = pl.strings[int(attr.NS)]
			nss := strings.Split(ns, "/")
			ns = nss[len(nss)-1]
		}

		val := ""
		if attr.RawValue != NoEntry {
			val = pl.strings[int(attr.RawValue)]
		} else {
			switch attr.TypedValue.Type {
			case DataIntDec:
				val = fmt.Sprintf("%v", attr.TypedValue.Value)
			case DataIntBool:
				val = fmt.Sprintf("%v", attr.TypedValue.Value == 0xFFFFFFFF)
			default:
				val = fmt.Sprintf("0x%08X", attr.TypedValue.Value)
			}
		}
		dt := attr.TypedValue.Type

		t.Logf("%s|attr:ns(%v) name(%s) val(%s) valtyp(%s)\n", ws, ns, pl.strings[int(attr.Name)], val, dt)
	}
	t.Logf("\n")
	for _, e := range el.Children {
		printrecurse(t, pl, e, ws+"  ")
	}
}

func TestBootstrap(t *testing.T) {
	r := require.New(t)
	bin, err := os.ReadFile("testdata/bootstrap.bin")
	r.NoError(err)

	// unmarshal binary xml and store byte indices of decoded resources.
	debugIndices := make(map[encoding.BinaryMarshaler]int)
	trackUnmarshal := func(buf []byte) (*XML, error) {
		bx := new(XML)
		err = (&bx.chunkHeader).UnmarshalBinary(buf)
		r.NoError(err)

		buf = buf[8:]
		debugIndex := 8
		for len(buf) > 0 {
			k, err := bx.unmarshalBinaryKind(buf)
			r.NoError(err)
			debugIndices[k.(encoding.BinaryMarshaler)] = debugIndex
			debugIndex += k.size()
			buf = buf[k.size():]
		}
		return bx, nil
	}

	checkMarshal := func(res encoding.BinaryMarshaler, bsize int) {
		b, err := res.MarshalBinary()
		r.NoError(err)
		idx := debugIndices[res]
		a := bin[idx : idx+bsize]
		if !bytes.Equal(a, b) {
			x, y := len(a), len(b)
			if x != y {
				t.Errorf("%v: %T: byte length does not match, have %v, want %v", idx, res, y, x)
			}
			if x > y {
				x, y = y, x
			}
			mismatch := false
			for i := 0; i < x; i++ {
				if mismatch = a[i] != b[i]; mismatch {
					t.Errorf("%v: %T: first byte mismatch at %v of %v", idx, res, i, bsize)
					break
				}
			}
			if mismatch {
				// print out a reasonable amount of data to help identify issues
				truncate := x > 1300
				if truncate {
					x = 1300
				}
				t.Log("      HAVE               WANT")
				for i := 0; i < x; i += 4 {
					he, we := 4, 4
					if i+he >= x {
						he = x - i
					}
					if i+we >= y {
						we = y - i
					}
					t.Logf("%3v | % X        % X\n", i, b[i:i+he], a[i:i+we])
				}
				if truncate {
					t.Log("... output truncated.")
				}
			}
		}
	}

	bxml, err := trackUnmarshal(bin)
	r.NoError(err)

	for i, x := range bxml.Pool.strings {
		t.Logf("Pool(%v): %q\n", i, x)
	}

	for _, e := range bxml.Children {
		printrecurse(t, bxml.Pool, e, "")
	}

	checkMarshal(&bxml.chunkHeader, int(bxml.headerByteSize))
	checkMarshal(bxml.Pool, bxml.Pool.size())
	checkMarshal(bxml.Map, bxml.Map.size())
	checkMarshal(bxml.Namespace, bxml.Namespace.size())

	for el := range bxml.iterElements() {
		checkMarshal(el, el.size())
		checkMarshal(el.end, el.end.size())
	}

	checkMarshal(bxml.Namespace.end, bxml.Namespace.end.size())
	checkMarshal(bxml, bxml.size())
}

func TestEncode(t *testing.T) {
	r := require.New(t)
	f, err := os.Open("testdata/bootstrap.xml")
	r.NoError(err)

	bx, err := UnmarshalXML(f, false, 30)
	r.NoError(err)

	bin, err := os.ReadFile("testdata/bootstrap.bin")
	r.NoError(err)

	bxml := new(XML)
	err = bxml.UnmarshalBinary(bin)
	r.NoError(err)

	err = compareStrings(t, bxml.Pool.strings, bx.Pool.strings)
	r.NoError(err)

	err = compareUint32s(t, rtou(bxml.Map.rs), rtou(bx.Map.rs))
	r.NoError(err)

	err = compareNamespaces(bx.Namespace, bxml.Namespace)
	r.NoError(err)

	err = compareElements(bx, bxml)
	r.NoError(err)
}

func TestRawValueByName(t *testing.T) {
	r := require.New(t)
	f, err := os.Open("testdata/bootstrap.xml")
	r.NoError(err)

	bx, err := UnmarshalXML(f, false, 30)
	r.NoError(err)

	pkgname, err := bx.RawValueByName("manifest", xml.Name{Local: "package"})
	r.NoError(err)
	r.Equal("com.zentus.balloon", pkgname)
}

type byAttrName []*Attribute

func (a byAttrName) Len() int           { return len(a) }
func (a byAttrName) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a byAttrName) Less(i, j int) bool { return a[i].Name < a[j].Name }

func compareElements(have, want *XML) error {
	h, w := have.iterElements(), want.iterElements()
	buf := new(bytes.Buffer)
	for {
		a, b := <-h, <-w
		if a == nil || b == nil {
			break
		}
		if a.Name.Resolve(have.Pool) == "uses-sdk" {
			a = <-h // discard uses-sdk token from tests since it's synthesized internally
		}
		if a.NS != b.NS ||
			a.Name != b.Name {
			return fmt.Errorf("elements don't match, have %+v, want %+v", a, b)
		}
		if a.end.NS != b.end.NS ||
			a.end.Name != b.end.Name {
			return fmt.Errorf("element ends don't match, have %+v, want %+v", a.end, b.end)
		}
		if len(a.attrs) != len(b.attrs) {
			return fmt.Errorf("element attribute lengths don't match, have %v, want %v", len(a.attrs), len(b.attrs))
		}

		// discards order of aapt and binres as some sorting details of apt have eluded this package but do not
		// affect final output from functioning correctly
		sort.Sort(byAttrName(a.attrs))
		sort.Sort(byAttrName(b.attrs))

		for i, attr := range a.attrs {
			bttr := b.attrs[i]
			if attr.NS != bttr.NS ||
				attr.Name != bttr.Name ||
				attr.RawValue != bttr.RawValue ||
				attr.TypedValue.Type != bttr.TypedValue.Type ||
				attr.TypedValue.Value != bttr.TypedValue.Value {
				// single exception to check for minSdkVersion which has peculiar output from aapt
				// but following same format of all other like-types appears to work correctly.
				// BUG(dskinner) this check is brittle as it will skip over any attribute in
				// bootstrap.xml that has value == MinSDK.
				if attr.TypedValue.Value == MinSDK {
					continue
				}
				fmt.Fprintf(buf, "attrs don't match\nhave: %+v\nwant: %+v\n", attr, bttr)
			}
		}
		if buf.Len() > 0 {
			buf.WriteString("-------------\n")
		}
	}
	if buf.Len() > 0 {
		return errors.New(buf.String())
	}
	return nil
}

func compareNamespaces(have, want *Namespace) error {
	if have == nil || want == nil ||
		have.LineNumber != want.LineNumber ||
		have.Comment != want.Comment ||
		have.prefix != want.prefix ||
		have.uri != want.uri {
		return fmt.Errorf("namespaces don't match, have %+v, want %+v", have, want)
	}
	if have.end != nil || want.end != nil {
		return compareNamespaces(have.end, want.end)
	}
	return nil
}

func rtou(a []TableRef) (b []uint32) {
	for _, x := range a {
		b = append(b, uint32(x))
	}
	return
}

func compareUint32s(t *testing.T, a, b []uint32) error {
	var err error
	if len(a) != len(b) {
		err = fmt.Errorf("lengths do not match")
	}

	n := len(a)
	if n < len(b) {
		n = len(b)
	}

	var buf bytes.Buffer
	buf.WriteString("a.Map.rs    b.Map.rs\n")
	for i := 0; i < n; i++ {
		var c, d string
		if i < len(a) {
			c = fmt.Sprintf("%0#8x ", a[i])
		} else {
			c = "__________ "
		}
		if i < len(b) {
			d = fmt.Sprintf("%0#8x ", b[i])
		} else {
			d = "__________ "
		}
		if err == nil && c != d {
			err = fmt.Errorf("has missing/incorrect values")
		}
		buf.WriteString(c + " " + d + "\n")
	}

	if err != nil {
		err = fmt.Errorf("%s\n%s", err, buf.String())
	}

	return err
}

func compareStrings(t *testing.T, a, b []string) error {
	var err error
	if len(a) != len(b) {
		err = fmt.Errorf("lengths do not match")
	}

	buf := new(bytes.Buffer)

	for i, x := range a {
		v := "__"
		for j, y := range b {
			if x == y {
				v = fmt.Sprintf("%2v", j)
				break
			}
		}
		if err == nil && v == "__" {
			if !strings.HasPrefix(x, "4.0.") {
				// as of the time of this writing, the current version of build tools being targeted
				// reports 4.0.4-1406430. Previously, this was 4.0.3. This number is likely still due
				// to change so only report error if 4.x incremented.
				//
				// TODO this check has the potential to hide real errors but can be fixed once more
				// of the xml document is unmarshalled and XML can be queried to assure this is related
				// to platformBuildVersionName.
				err = fmt.Errorf("has missing/incorrect values")
			}
		}
		fmt.Fprintf(buf, "Pool(%2v, %s) %q\n", i, v, x)
	}

	contains := func(xs []string, a string) bool {
		for _, x := range xs {
			if x == a {
				return true
			}
		}
		return false
	}

	if err != nil {
		buf.WriteString("\n## only in var a\n")
		for i, x := range a {
			if !contains(b, x) {
				fmt.Fprintf(buf, "Pool(%2v) %q\n", i, x)
			}
		}

		buf.WriteString("\n## only in var b\n")
		for i, x := range b {
			if !contains(a, x) {
				fmt.Fprintf(buf, "Pool(%2v) %q\n", i, x)
			}
		}
	}

	if err != nil {
		err = fmt.Errorf("%s\n%s", err, buf.String())
	}

	return err
}

func TestOpenTable(t *testing.T) {
	r := require.New(t)
	sdkdir := os.Getenv("ANDROID_HOME")
	if sdkdir == "" {
		t.Skip("ANDROID_HOME env var not set")
	}
	tbl, err := OpenTable()
	r.NoError(err)

	if len(tbl.pkgs) == 0 {
		t.Fatal("failed to decode any resource packages")
	}

	pkg := tbl.pkgs[0]

	t.Log("package name:", pkg.name)

	for i, x := range pkg.typePool.strings {
		t.Logf("typePool[i=%v]: %s\n", i, x)
	}

	for i, spec := range pkg.specs {
		t.Logf("spec[i=%v]: %v %q\n", i, spec.id, pkg.typePool.strings[spec.id-1])
		for j, typ := range spec.types {
			t.Logf("\ttype[i=%v]: %v\n", j, typ.id)
			for k, nt := range typ.entries {
				if nt == nil { // NoEntry
					continue
				}
				t.Logf("\t\tentry[i=%v]: %v %q\n", k, nt.key, pkg.keyPool.strings[nt.key])
				if k > 5 {
					t.Logf("\t\t... truncating output")
					break
				}
			}
		}
	}
}

func TestTableRefByName(t *testing.T) {
	r := require.New(t)
	checkResources(t)
	tbl, err := OpenSDKTable()
	r.NoError(err)

	if len(tbl.pkgs) == 0 {
		t.Fatal("failed to decode any resource packages")
	}

	ref, err := tbl.RefByName("@android:style/Theme.NoTitleBar.Fullscreen")
	r.NoError(err)

	if want := uint32(0x01030007); uint32(ref) != want {
		t.Fatalf("RefByName does not match expected result, have %0#8x, want %0#8x", ref, want)
	}
}

func TestTableMarshal(t *testing.T) {
	r := require.New(t)

	checkResources(t)
	tbl, err := OpenSDKTable()
	r.NoError(err)

	bin, err := tbl.MarshalBinary()
	r.NoError(err)

	xtbl := new(Table)
	err = xtbl.UnmarshalBinary(bin)
	r.NoError(err)

	r.Equal(len(tbl.pool.strings), len(xtbl.pool.strings))
	r.Equal(len(tbl.pkgs), len(xtbl.pkgs))

	pkg, xpkg := tbl.pkgs[0], xtbl.pkgs[0]
	err = compareStrings(t, pkg.typePool.strings, xpkg.typePool.strings)
	r.NoError(err)

	err = compareStrings(t, pkg.keyPool.strings, xpkg.keyPool.strings)
	r.NoError(err)

	r.Equal(pkg.specs, xpkg.specs)
}

func checkResources(t *testing.T) {
	t.Helper()
	sdkdir := os.Getenv("ANDROID_HOME")
	if sdkdir == "" {
		t.Skip("ANDROID_HOME env var not set")
	}
	rscPath, err := apiResourcesPath()
	if err != nil {
		t.Skipf("failed to find resources: %v", err)
	}
	if _, err := os.Stat(rscPath); err != nil {
		t.Skipf("failed to find resources: %v", err)
	}
}

func BenchmarkTableRefByName(b *testing.B) {
	sdkdir := os.Getenv("ANDROID_HOME")
	if sdkdir == "" {
		b.Fatal("ANDROID_HOME env var not set")
	}

	b.ReportAllocs()
	b.ResetTimer()
	for n := 0; n < b.N; n++ {
		tbl, err := OpenTable()
		if err != nil {
			b.Fatal(err)
		}
		_, err = tbl.RefByName("@android:style/Theme.NoTitleBar.Fullscreen")
		if err != nil {
			b.Fatal(err)
		}
	}
}
