// textdump-tester -- test utility for Python's textdump-tester
// Copyright (C) 2024-2025 Sergey Matveev <stargrave@stargrave.org>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as
// published by the Free Software Foundation, version 3 of the License.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this program.  If not, see <http://www.gnu.org/licenses/>.

// Look for py3/tests/textdump-tester tools for more description.

package main

import (
	"bufio"
	"bytes"
	"encoding/hex"
	"io"
	"log"
	"math/big"
	"os"
	"strconv"
	"strings"
	"time"

	"go.cypherpunks.su/keks"
	"go.cypherpunks.su/tai64n/v4"
)

var Scanner *bufio.Scanner

func getFields() []string {
	if !Scanner.Scan() {
		if err := Scanner.Err(); err != nil {
			log.Fatal(err)
		}
		return []string{""}
	}
	t := Scanner.Text()
	if t == "" {
		return []string{""}
	}
	return strings.Fields(t)
}

func mustDecodeHex(s string) []byte {
	b, err := hex.DecodeString(s)
	if err != nil {
		log.Fatal(err)
	}
	return b
}

func checker(v any) {
	fields := getFields()
	switch f := fields[0]; f {
	case "NIL":
		if v != nil {
			log.Fatalf("expected NIL, got %+v\n", v)
		}
	case "FALSE":
		our, ok := v.(bool)
		if !ok {
			log.Fatalf("expected bool, got %+v\n", v)
		}
		if our {
			log.Fatalf("expected FALSE, got %+v\n", our)
		}
	case "TRUE":
		our, ok := v.(bool)
		if !ok {
			log.Fatalf("expected bool, got %+v\n", v)
		}
		if !our {
			log.Fatalf("expected TRUE, got %+v\n", our)
		}
	case "HEXLET":
		our, ok := v.(*keks.Hexlet)
		if !ok {
			log.Fatalf("expected HEXLET, got %+v\n", v)
		}
		their := keks.Hexlet(mustDecodeHex(fields[1]))
		if *our != their {
			log.Fatalln("HEXLET differs:", our, their)
		}
	case "UTC":
		our, ok := v.(time.Time)
		if !ok {
			log.Fatalf("expected Time, got %+v\n", v)
		}
		var their time.Time
		var err error
		if our.Nanosecond() > 0 {
			their, err = time.Parse(time.RFC3339Nano, fields[1])
		} else {
			their, err = time.Parse(time.RFC3339, fields[1])
		}
		if err != nil {
			log.Fatal(err)
		}
		if !their.Equal(our) {
			log.Fatalln("UTC differs:", our, their)
		}
	case "TAI64NA":
		our, ok := v.(*tai64n.TAI64NA)
		if !ok {
			log.Fatalf("expected TAI64NA, got %+v\n", v)
		}
		their := tai64n.TAI64NA(mustDecodeHex(fields[1]))
		if *our != their {
			log.Fatalln("TAI64NA differs:", our, their)
		}
	case "MAGIC":
		our, ok := v.(keks.Magic)
		if !ok {
			log.Fatalf("expected Magic, got %+v\n", v)
		}
		var their string
		if len(fields) > 1 {
			their = string(mustDecodeHex(fields[1]))
		}
		if our != keks.Magic(their) {
			log.Fatalln("MAGIC differs:", our, their)
		}
	case "BIN":
		our, ok := v.([]byte)
		if !ok {
			log.Fatalf("expected []byte, got %+v\n", v)
		}
		var their []byte
		if len(fields) > 1 {
			their = mustDecodeHex(fields[1])
		}
		if !bytes.Equal(our, their) {
			log.Fatalln("BIN differs:", our, their)
		}
	case "STR":
		our, ok := v.(string)
		if !ok {
			log.Fatalf("expected string, got %+v\n", v)
		}
		var their string
		if len(fields) > 1 {
			their = string(mustDecodeHex(fields[1]))
		}
		if our != their {
			log.Fatalln("STR differs:", our, their)
		}
	case "INT":
		their, ok := new(big.Int).SetString(fields[1], 10)
		if !ok {
			log.Fatal("can not parse INT")
		}
		if their.Sign() >= 0 {
			if their.BitLen() > 64 {
				goto BigIntCheck
			}
			var our uint64
			our, ok = v.(uint64)
			if !ok {
				log.Fatalf("expected uint64, got %+v\n", v)
			}
			if our != their.Uint64() {
				log.Fatalln("INT differs:", our, their.Uint64())
			}
		} else {
			if their.BitLen() > 63 {
				goto BigIntCheck
			}
			var our int64
			our, ok = v.(int64)
			if !ok {
				log.Fatalf("expected int64, got %+v\n", v)
			}
			if our != their.Int64() {
				log.Fatalln("INT differs:", our, their.Int64())
			}
		}
		break
	BigIntCheck:
		our, ok := v.(*big.Int)
		if !ok {
			log.Fatalf("expected big.Int, got %+v\n", v)
		}
		if our.Cmp(their) != 0 {
			log.Fatalln("INT differs:", our, their)
		}
	case "BLOB":
		blob, ok := v.(keks.BlobChunked)
		if !ok {
			log.Fatalf("expected BlobChunked, got %+v\n", v)
		}
		chunkLen, err := strconv.Atoi(fields[1])
		if err != nil {
			log.Fatal(err)
		}
		if blob.ChunkLen != int64(chunkLen) {
			log.Fatalln("chunkLen differs:", blob.ChunkLen, chunkLen)
		}
		var their []byte
		if len(fields) > 2 {
			their = mustDecodeHex(fields[2])
		}
		our, err := io.ReadAll(blob.Reader())
		if err != nil {
			log.Fatal(err)
		}
		if !bytes.Equal(our, their) {
			log.Fatalln("BLOB differs:", our, their)
		}
	case "LIST":
		our, ok := v.([]any)
		if !ok {
			log.Fatalf("expected []any, got %+v\n", v)
		}
		their, err := strconv.Atoi(fields[1])
		if err != nil {
			log.Fatal(err)
		}
		if len(our) != their {
			log.Fatalln("LIST len differs:", our, their)
		}
		for _, item := range our {
			checker(item)
		}
	case "MAP":
		our, ok := v.(map[string]any)
		if !ok {
			log.Fatalf("expected map[string]any, got %+v\n", v)
		}
		their, err := strconv.Atoi(fields[1])
		if err != nil {
			log.Fatal(err)
		}
		if len(our) != their {
			log.Fatalln("MAP len differs:", our, their)
		}
		for range their {
			fields = getFields()
			k := string(mustDecodeHex(fields[1]))
			item, ok := our[k]
			if !ok {
				log.Fatalln("no key found:", k)
			}
			checker(item)
		}
	default:
		log.Fatalln("unknown:", f)
	}
}

func main() {
	log.SetFlags(log.Lshortfile)
	Scanner = bufio.NewScanner(os.Stdin)
	var fields []string
	for {
		fields = getFields()
		if fields[0] != "KEKS" {
			log.Fatalln("KEKS was expected, got", fields[0])
		}
		their := mustDecodeHex(fields[1])
		decoder := keks.NewDecoderFromBytes(their, nil)
		v, err := decoder.Decode()
		if err != nil {
			log.Fatal(err)
		}
		log.Println("parsed:", decoder.Read, "bytes")
		{
			our, err := keks.EncodeBuf(v, nil)
			if err != nil {
				log.Fatal(err)
			}
			if !bytes.Equal(our, their) {
				log.Fatal("encoded version differs")
			}
		}
		checker(v)
		fields = getFields()
		if fields[0] != "EOC" {
			log.Fatalln("EOC was expected, got", fields[0])
		}
	}
}
