import json
import platform
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
from math import isinf, isnan
from pathlib import Path
from typing import Any

import pytest

from boabem import Context, PanicException, Undefined


def test_banana():
    ctx = Context()
    assert ctx.eval('"b" + "a" + +"a" + "a"') == "baNaNa"


@pytest.mark.parametrize(
    ("code", "expect"),
    [
        (
            "(![] + [])[+[]] + (![] + [])[+!+[]] + ([![]] + [][[]])[+!+[] + [+[]]] + (![] + [])[!+[] + !+[]]",
            "fail",
        ),
        ("10n ** 100n", 10**100),
        ("-(10n ** 100n)", -(10**100)),
        ('parseInt("f*ck", 16)', 15),
        ('parseInt("Infinity", 35)', 1201203301724),
        ("parseInt(null, 24)", 23),
        ("(true + true) * (true + true) - true", 3),
        ("10000000000000000 + 1.1", 10000000000000002),
        ("'3' + 1", "31"),
        ("{} + []", 0),
        ("({ [{}]: { [{}]: {} } }[{}][{}])", {}),
        ("[...[...'...']].length", 3),
        ("[10, 1, 3].sort()", [1, 10, 3]),
    ],
)
def test_equal(code: str, expect: Any):
    ctx = Context()
    assert ctx.eval(code) == expect


@pytest.mark.parametrize(
    ("code", "expect"),
    [
        ("true == []", False),
        ("true == ![]", False),
        ("false == []", True),
        ("false == ![]", True),
        ("![]", False),
        ('!!"false" == !!"true"', True),
        ('!!"false" === !!"true"', True),
        ("Object.is(NaN, NaN)", True),
        ("NaN === NaN", False),
        ("Object.is(-0, 0)", False),
        ("-0 === 0", True),
        ("[[[[[[]]]]]] == ''", True),
        ("[[[[[[ undefined ]]]]]] == 0", True),
        ("1 < 2 < 3", True),
        ("3 > 2 > 1", False),
        ("(1).__proto__.__proto__.__proto__", None),
    ],
)
def test_is(code: str, expect: Any):
    ctx = Context()
    assert ctx.eval(code) is expect


def test_boolean_logic():
    ctx = Context()
    assert ctx.eval("true && false") is False
    assert ctx.eval("true || false") is True
    assert ctx.eval("!false") is True


def test_string_concat_unicode():
    ctx = Context()
    result = ctx.eval("'한글' + '😊'")
    assert result == "한글😊"


def test_null_and_undefined_are_distinct():
    ctx = Context()
    js_null = ctx.eval("null")
    js_undef = ctx.eval("undefined")

    assert js_null is None
    assert js_undef != js_null
    assert isinstance(js_undef, Undefined)


def test_exception_propagation():
    ctx = Context()
    with pytest.raises(RuntimeError, match="boom"):
        ctx.eval("throw new Error('boom')")


def test_context_state_persists_between_evals():
    ctx = Context()
    ctx.eval("var x = 41")
    assert ctx.eval("x + 1") == 42


def test_eval_from_filepath(tmp_path: Path):
    p = tmp_path / "script.js"
    p.write_text("1 + 2", encoding="utf-8")

    ctx = Context()
    result = ctx.eval_from_filepath(p)
    assert result == 3

    p.write_text("3 + 4", encoding="utf-8")

    ctx = Context()
    result = ctx.eval_from_filepath(str(p))
    assert result == 7


def test_nan_and_infinity():
    ctx = Context()
    assert isnan(ctx.eval("NaN"))
    assert isinf(ctx.eval("Infinity"))
    assert isinf(ctx.eval("-Infinity"))
    assert ctx.eval("Number.isNaN(NaN)") is True
    assert ctx.eval("isNaN(0/0)") is True
    assert ctx.eval("isFinite(Infinity)") is False
    assert ctx.eval("isFinite(1/0)") is False


def test_syntax_error_raises():
    ctx = Context()
    with pytest.raises(RuntimeError, match="SyntaxError"):
        ctx.eval("function(")


def test_bigint_number_mixing_raises():
    ctx = Context()
    with pytest.raises(RuntimeError, match="BigInt"):
        ctx.eval("1n + 1")


def test_template_literals():
    ctx = Context()
    result = ctx.eval("`Hello ${21 + 21}`")
    assert result == "Hello 42"


def test_function_definition_and_call_across_evals():
    ctx = Context()
    ctx.eval("function inc(x) { return x + 1 }")
    assert ctx.eval("inc(41)") == 42
    assert ctx.eval('inc("41")') == "411"


def test_try_catch_returns_message():
    ctx = Context()
    result = ctx.eval("try { throw new Error('boom') } catch (e) { e.message }")
    assert result == "boom"


def test_eval_from_bytes_supports_utf8():
    ctx = Context()
    code_str = "'A' + 'B'"
    res = ctx.eval_from_bytes(code_str)
    assert res == "AB"


def test_multiline_script():
    ctx = Context()
    result = ctx.eval("const a = 5;\nconst b = 7;\na * b")
    assert result == 35


def test_array_length():
    ctx = Context()
    assert ctx.eval("[1,2,3,4].length") == 4


def test_json_stringify_roundtrip():
    ctx = Context()
    js = ctx.eval("JSON.stringify({a:1, b:[2,3], c:'x'})")
    assert js == '{"a":1,"b":[2,3],"c":"x"}'
    obj = json.loads(js)
    assert obj == {"a": 1, "b": [2, 3], "c": "x"}


def test_bigint_loose_and_strict_equality():
    ctx = Context()
    # 느슨한 동등 비교는 true, 엄격한 동등 비교는 false
    assert ctx.eval("1n == 1") is True
    assert ctx.eval("1n === 1") is False


def test_truthiness():
    ctx = Context()
    assert ctx.eval("Boolean({})") is True
    assert ctx.eval("Boolean([])") is True
    assert ctx.eval("Boolean(0)") is False
    assert ctx.eval("Boolean('')") is False


def test_reference_error_for_undefined_identifier():
    ctx = Context()
    # 선언되지 않은 식별자 접근은 ReferenceError
    with pytest.raises(RuntimeError, match="ReferenceError"):
        ctx.eval("notDeclared + 1")


def test_typeof_undeclared_identifier():
    ctx = Context()
    # typeof undeclared 는 예외가 아니라 'undefined'
    assert ctx.eval("typeof notDeclared") == "undefined"


def test_strict_mode_assignment_error():
    ctx = Context()
    with pytest.raises(RuntimeError):
        ctx.eval("'use strict'; x = 3.14")  # 선언 없이 할당 금지


def test_regexp_test_and_exec():
    ctx = Context()
    assert ctx.eval("/foo/.test('foobar')") is True
    assert ctx.eval("/bar/.test('baz')") is False


def test_date_to_string_typeof():
    ctx = Context()
    assert ctx.eval("typeof new Date()") == "object"
    assert ctx.eval("new Date(0).toUTCString()") == "Thu, 01 Jan 1970 00:00:00 GMT"


def test_destructuring_and_spread():
    ctx = Context()
    assert ctx.eval("let {a,b} = {a:1,b:2}; a+b") == 3
    assert ctx.eval("let arr = [1,2,3]; Math.max(...arr)") == 3


def test_parse_int_float_and_math():
    ctx = Context()
    assert ctx.eval("parseInt('10', 10)") == 10
    assert ctx.eval("parseFloat('3.14')") == 3.14
    assert ctx.eval("Math.min(5, -1, 3)") == -1


def test_bitwise_operations():
    ctx = Context()
    assert ctx.eval("5 & 3") == (5 & 3)
    assert ctx.eval("5 | 2") == (5 | 2)
    assert ctx.eval("5 ^ 1") == (5 ^ 1)
    assert ctx.eval("~0") == (~0)
    assert ctx.eval("1 << 5") == (1 << 5)
    assert ctx.eval("(32 >> 2)") == (32 >> 2)
    assert ctx.eval("(-1 >>> 1) >= 0") is True  # JS 무부호 시프트 결과는 32-bit 양수


def test_short_circuiting():
    ctx = Context()
    assert ctx.eval("false && (x=1)") is False
    assert ctx.eval("true || (x=1)") is True


def test_json_parse_roundtrip():
    ctx = Context()
    code = """JSON.parse('{"a":1}').a"""
    assert ctx.eval(code) == 1


def test_unicode_escape_in_string():
    ctx = Context()
    assert ctx.eval("'\ud55c\uae00' + '!' ") == "한글!"


def test_remainder_negative():
    ctx = Context()
    # JS의 나머지 연산은 피제수의 부호를 따름(-5 % 2 == -1)
    assert ctx.eval("-5 % 2") == -1


def test_bigint_division_truncates():
    ctx = Context()
    # BigInt 나눗셈은 소수 버림
    assert ctx.eval("5n / 2n") == 2


def test_delete_property():
    ctx = Context()
    assert ctx.eval("let o={a:1,b:2}; delete o.a; 'a' in o") is False


def test_typeof_null_and_function_length():
    ctx = Context()
    assert ctx.eval("typeof null") == "object"
    assert ctx.eval("(function(a,b){}).length === 2") is True


def test_arrow_function_call():
    ctx = Context()
    assert ctx.eval("(() => 42)()") == 42


def test_const_reassignment_throws():
    ctx = Context()
    with pytest.raises(RuntimeError):
        ctx.eval("const a=1; a=2")


def test_bigint_unary_plus_throws_and_to_string_radix():
    ctx = Context()
    with pytest.raises(RuntimeError):
        ctx.eval("+1n")
    assert ctx.eval("(255n).toString(16)") == "ff"


def test_map_and_set_size():
    ctx = Context()
    assert ctx.eval("new Map([[1,2],[3,4]]).size") == 2
    assert ctx.eval("new Set([1,1,2]).size") == 2


def test_date_utc_epoch():
    ctx = Context()
    assert ctx.eval("Date.UTC(1970,0,1)") == 0


def test_encode_decode_uri_component():
    ctx = Context()
    assert ctx.eval("decodeURIComponent(encodeURIComponent('한글'))") == "한글"


def test_switch_and_for_sum_via_iife():
    ctx = Context()
    assert (
        ctx.eval(
            "(() => { const x=2; switch(x){case 1: return 'foo'; case 2: return 'bar'; default: return 'baz'; } })()"
        )
        == "bar"
    )
    assert (
        ctx.eval("(() => { let s=0; for(let i=1;i<=5;i++){ s+=i } return s })()") == 15
    )


def test_number_coercion_and_parse_int():
    ctx = Context()
    assert ctx.eval("Number('  10  ')") == 10
    assert ctx.eval("parseInt('08', 10)") == 8


def test_delete_non_configurable_property_returns_false():
    ctx = Context()
    assert ctx.eval("delete Math.PI") is False


def test_weird_equality_cases():
    ctx = Context()
    assert ctx.eval("[] == ![]") is True
    assert ctx.eval("0 == false") is True
    assert ctx.eval("0 === false") is False


def test_number_is_integer():
    ctx = Context()
    assert ctx.eval("Number.isInteger(3.0)") is True
    assert ctx.eval("Number.isInteger(3.1)") is False


def test_cannot_convert_symbol():
    ctx = Context()
    with pytest.raises(RuntimeError, match="TypeError: cannot convert Symbol"):
        ctx.eval("Symbol('a')")


def test_undefined_is_same():
    ctx = Context()
    undefined1 = ctx.eval("undefined")
    undefined2 = ctx.eval("")
    assert isinstance(undefined1, Undefined)
    assert isinstance(undefined2, Undefined)

    assert ctx.eval("undefined == undefined") is True
    assert ctx.eval("undefined === undefined") is True
    assert undefined1 == undefined2

    assert ctx.eval("Object.is(undefined, undefined)") is True
    assert undefined1 is not undefined2


def test_thread_pool():
    ctx = Context()
    with ThreadPoolExecutor() as executor:
        future = executor.submit(ctx.eval, "1 + 1")

    # pyo3_runtime.PanicException
    with pytest.raises(PanicException, match="unsendable"):
        future.result()


@pytest.mark.xfail(platform.python_implementation() == "PyPy", reason="pypy")
def test_process_pool():
    ctx = Context()
    with ProcessPoolExecutor() as executor:
        future = executor.submit(ctx.eval, "1 + 1")

    with pytest.raises(TypeError, match="cannot pickle"):
        future.result()


def test_url_basic():
    ctx = Context()
    ctx.eval(
        "url = new URL('https://example.com:8080/path/to/resource?query#fragment')"
    )
    assert ctx.eval("url instanceof URL") is True
    assert (
        ctx.eval("url.href")
        == "https://example.com:8080/path/to/resource?query#fragment"
    )
    assert ctx.eval("url.protocol") == "https:"
    assert ctx.eval("url.host") == "example.com:8080"
    assert ctx.eval("url.hostname") == "example.com"
    assert ctx.eval("url.port") == "8080"
    assert ctx.eval("url.pathname") == "/path/to/resource"
    assert ctx.eval("url.search") == "?query"
    assert ctx.eval("url.hash") == "#fragment"


def test_url_static_methods():
    ctx = Context()
    assert (
        ctx.eval('URL.canParse("http://example.org/new/path?new-query#new-fragment")')
        is True
    )
    assert (
        ctx.eval('!URL.canParse("http//:example.org/new/path?new-query#new-fragment")')
        is True
    )
    assert (
        ctx.eval(
            '!URL.canParse("http://example.org/new/path?new-query#new-fragment", "http:")'
        )
        is True
    )
    assert (
        ctx.eval(
            'URL.canParse("/new/path?new-query#new-fragment", "http://example.org/")'
        )
        is True
    )
