package jsnum

import (
	"flag"
	"fmt"
	"math"
	"os"
	"path/filepath"
	"slices"
	"testing"

	"efprojects.com/kitten-ipc/kitcom/internal/tsgo/testutil/jstest"
	"github.com/go-json-experiment/json"
	"gotest.tools/v3/assert"
)

type stringTest struct {
	number Number
	str    string
}

var stringTests = slices.Concat([]stringTest{
	{NaN(), "NaN"},
	{Inf(1), "Infinity"},
	{Inf(-1), "-Infinity"},
	{0, "0"},
	{negativeZero, "0"},
	{1, "1"},
	{-1, "-1"},
	{0.3, "0.3"},
	{-0.3, "-0.3"},
	{1.5, "1.5"},
	{-1.5, "-1.5"},
	{1e308, "1e+308"},
	{-1e308, "-1e+308"},
	{math.Pi, "3.141592653589793"},
	{-math.Pi, "-3.141592653589793"},
	{MaxSafeInteger, "9007199254740991"},
	{MinSafeInteger, "-9007199254740991"},
	{numberFromBits(0x000FFFFFFFFFFFFF), "2.225073858507201e-308"},
	{numberFromBits(0x0010000000000000), "2.2250738585072014e-308"},
	{1234567.8, "1234567.8"},
	{19686109595169230000, "19686109595169230000"},
	{123.456, "123.456"},
	{-123.456, "-123.456"},
	{444123, "444123"},
	{-444123, "-444123"},
	{444123.789123456789875436, "444123.7891234568"},
	{-444123.78963636363636363636, "-444123.7896363636"},
	{1e21, "1e+21"},
	{1e20, "100000000000000000000"},
}, ryuTests)

func TestString(t *testing.T) {
	t.Parallel()

	for _, test := range stringTests {
		fInput := float64(test.number)

		t.Run(fmt.Sprintf("%v", fInput), func(t *testing.T) {
			t.Parallel()
			assert.Equal(t, test.number.String(), test.str)
		})
	}
}

var fromStringTests = []stringTest{
	{NaN(), "    NaN"},
	{Inf(1), "Infinity    "},
	{Inf(-1), "    -Infinity"},
	{1, "1."},
	{1, "1.0   "},
	{1, "+1"},
	{1, "+1."},
	{1, "+1.0"},
	{NaN(), "whoops"},
	{0, ""},
	{0, "0"},
	{0, "0."},
	{0, "0.0"},
	{0, "0.0000"},
	{0, ".0000"},
	{negativeZero, "-0"},
	{negativeZero, "-0."},
	{negativeZero, "-0.0"},
	{negativeZero, "-.0"},
	{NaN(), "."},
	{NaN(), "e"},
	{NaN(), ".e"},
	{NaN(), "+"},
	{0, "0X0"},
	{NaN(), "e0"},
	{NaN(), "E0"},
	{NaN(), "1e"},
	{NaN(), "1e+"},
	{NaN(), "1e-"},
	{1, "1e+0"},
	{NaN(), "++0"},
	{NaN(), "0_0"},
	{Inf(1), "1e1000"},
	{Inf(-1), "-1e1000"},
	{0, ".0e0"},
	{NaN(), "0e++0"},
	{10, "0XA"},
	{0b1010, "0b1010"},
	{0b1010, "0B1010"},
	{0o12, "0o12"},
	{0o12, "0O12"},
	{0x123456789abcdef0, "0x123456789abcdef0"},
	{0x123456789abcdef0, "0X123456789ABCDEF0"},
	{18446744073709552000, "0X10000000000000000"},
	{18446744073709597000, "0X1000000000000A801"},
	{NaN(), "0B0.0"},
	{1.231235345083403e+91, "12312353450834030486384068034683603046834603806830644850340602384608368034634603680348603864"},
	{NaN(), "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX8OOOOOOOOOOOOOOOOOOO"},
	{Inf(1), "+Infinity"},
	{1234.56, "  \t1234.56  "},
	{NaN(), "\u200b"},
	{0, " "},
	{0, "\n"},
	{0, "\r"},
	{0, "\r\n"},
	{0, "\u2028"},
	{0, "\u2029"},
	{0, "\t"},
	{0, "\v"},
	{0, "\f"},
	{0, "\uFEFF"},
	{0, "\u00A0"},
	{10000000000000000000, "010000000000000000000"},
	{NaN(), "0x1.fffffffffffffp1023"}, // Make sure Go's extended float syntax doesn't work.
	{NaN(), "0X_1FFFP-16"},
	{NaN(), "1_000"}, // NumberToString doesn't handle underscores.
	{0, "0x0"},
	{0, "0X0"},
	{NaN(), "0xOOPS"},
	{0xABCDEF, "0xABCDEF"},
	{0xABCDEF, "0xABCDEF"},
	{0, "0o0"},
	{0, "0O0"},
	{NaN(), "0o8"},
	{NaN(), "0O8"},
	{0o12345, "0o12345"},
	{0o12345, "0O12345"},
	{0, "0b0"},
	{0, "0B0"},
	{NaN(), "0b2"},
	{NaN(), "0b2"},
	{0b10101, "0b10101"},
	{0b10101, "0B10101"},
	{NaN(), "1.f"},
	{NaN(), "1.e"},
	{NaN(), "1.0ef"},
	{NaN(), "1.0e"},
	{NaN(), ".f"},
	{NaN(), ".e"},
	{NaN(), ".0ef"},
	{NaN(), ".0e"},
	{NaN(), "a.f"},
	{NaN(), "a.e"},
	{NaN(), "a.0ef"},
	{NaN(), "a.0e"},
}

func TestFromString(t *testing.T) {
	t.Parallel()

	t.Run("stringTests", func(t *testing.T) {
		t.Parallel()
		for _, test := range stringTests {
			t.Run(test.str, func(t *testing.T) {
				t.Parallel()
				assertEqualNumber(t, FromString(test.str), test.number)
				assertEqualNumber(t, FromString(test.str+" "), test.number)
				assertEqualNumber(t, FromString(" "+test.str), test.number)
			})
		}
	})

	t.Run("fromStringTests", func(t *testing.T) {
		t.Parallel()
		for _, test := range fromStringTests {
			t.Run(test.str, func(t *testing.T) {
				t.Parallel()
				assertEqualNumber(t, FromString(test.str), test.number)
			})
		}
	})
}

func TestStringRoundtrip(t *testing.T) {
	t.Parallel()

	for _, test := range stringTests {
		t.Run(test.str, func(t *testing.T) {
			t.Parallel()
			assert.Equal(t, FromString(test.str).String(), test.str)
		})
	}
}

func TestStringJS(t *testing.T) {
	t.Parallel()
	jstest.SkipIfNoNodeJS(t)

	t.Run("stringTests", func(t *testing.T) {
		t.Parallel()

		// These tests should roundtrip both ways.
		stringTestsResults := getStringResultsFromJS(t, stringTests)
		for i, test := range stringTests {
			t.Run(fmt.Sprintf("%v", float64(test.number)), func(t *testing.T) {
				t.Parallel()
				assertEqualNumber(t, stringTestsResults[i].number, test.number)
				assert.Equal(t, stringTestsResults[i].str, test.str)
			})
		}
	})

	t.Run("fromStringTests", func(t *testing.T) {
		t.Parallel()

		// These tests should convert the string to the same number.
		fromStringTestsResults := getStringResultsFromJS(t, fromStringTests)
		for i, test := range fromStringTests {
			t.Run(fmt.Sprintf("fromString %q", test.str), func(t *testing.T) {
				t.Parallel()
				assertEqualNumber(t, fromStringTestsResults[i].number, test.number)
			})
		}
	})
}

func isFuzzing() bool {
	return flag.CommandLine.Lookup("test.fuzz").Value.String() != ""
}

func FuzzStringJS(f *testing.F) {
	jstest.SkipIfNoNodeJS(f)

	if isFuzzing() {
		// Avoid running anything other than regressions in the fuzzing mode.
		for _, test := range stringTests {
			f.Add(float64(test.number))
		}
		for _, test := range fromStringTests {
			f.Add(float64(test.number))
		}
	}

	f.Fuzz(func(t *testing.T, f float64) {
		n := Number(f)
		nStr := n.String()

		results := getStringResultsFromJS(t, []stringTest{{number: n, str: nStr}})
		assert.Equal(t, len(results), 1)

		nToJSStr := results[0].str
		nStrToJSNumber := results[0].number

		assert.Equal(t, nStr, nToJSStr)
		assertEqualNumber(t, n, nStrToJSNumber)
	})
}

func FuzzFromStringJS(f *testing.F) {
	jstest.SkipIfNoNodeJS(f)

	if isFuzzing() {
		// Avoid running anything other than regressions in the fuzzing mode.
		for _, test := range stringTests {
			f.Add(test.str)
		}
		for _, test := range fromStringTests {
			f.Add(test.str)
		}
	}

	f.Fuzz(func(t *testing.T, s string) {
		if len(s) > 350 {
			t.Skip()
		}

		n := FromString(s)
		results := getStringResultsFromJS(t, []stringTest{{str: s}})
		assert.Equal(t, len(results), 1)
		assertEqualNumber(t, n, results[0].number)
	})
}

func getStringResultsFromJS(t testing.TB, tests []stringTest) []stringTest {
	t.Helper()
	tmpdir := t.TempDir()

	type data struct {
		Bits [2]uint32 `json:"bits"`
		Str  string    `json:"str"`
	}

	inputData := make([]data, len(tests))
	for i, test := range tests {
		inputData[i] = data{
			Bits: numberToUint32Array(test.number),
			Str:  test.str,
		}
	}

	jsonInput, err := json.Marshal(inputData)
	assert.NilError(t, err)

	jsonInputPath := filepath.Join(tmpdir, "input.json")
	err = os.WriteFile(jsonInputPath, jsonInput, 0o644)
	assert.NilError(t, err)

	script := `
		import fs from 'fs';

		function fromBits(bits) {
			const buffer = new ArrayBuffer(8);
			(new Uint32Array(buffer))[0] = bits[0];
			(new Uint32Array(buffer))[1] = bits[1];
			return new Float64Array(buffer)[0];
		}

		function toBits(number) {
			const buffer = new ArrayBuffer(8);
			(new Float64Array(buffer))[0] = number;
			return [(new Uint32Array(buffer))[0], (new Uint32Array(buffer))[1]];
		}

		export default function(inputFile) {
			const input = JSON.parse(fs.readFileSync(inputFile, 'utf8'));

			const output = input.map((input) => ({
				str: ""+fromBits(input.bits),
				bits: toBits(+input.str),	
			}));

			return output;
		};
	`

	outputData, err := jstest.EvalNodeScript[[]data](t, script, tmpdir, jsonInputPath)
	assert.NilError(t, err)
	assert.Equal(t, len(outputData), len(tests))

	output := make([]stringTest, len(tests))
	for i, outputDatum := range outputData {
		output[i] = stringTest{
			number: uint32ArrayToNumber(outputDatum.Bits),
			str:    outputDatum.Str,
		}
	}

	return output
}

func numberToUint32Array(n Number) [2]uint32 {
	bits := numberToBits(n)
	return [2]uint32{uint32(bits), uint32(bits >> 32)}
}

func uint32ArrayToNumber(a [2]uint32) Number {
	bits := uint64(a[0]) | uint64(a[1])<<32
	return numberFromBits(bits)
}
