"""Tests for JavaScript function tracing.

Tests the JavaScript tracer implementation including:
- Unit tests for Python-side trace parsing and replay test generation
- End-to-end tests for the full tracing pipeline via trace-runner.js
"""

from __future__ import annotations

import json
import os
import shutil
import sqlite3
import subprocess
from pathlib import Path

import pytest

from codeflash.languages.javascript.replay_test import (
    JavaScriptFunctionModule,
    create_javascript_replay_test,
    get_function_alias,
    get_traced_functions_from_db,
)
from codeflash.languages.javascript.tracer import JavaScriptTracer


def node_available() -> bool:
    return shutil.which("node") is not None


def skip_if_node_not_available() -> None:
    if not node_available():
        pytest.skip("Node.js not available")


class TestJavaScriptTracerParsing:

    @pytest.fixture
    def trace_db_with_function_calls(self, tmp_path: Path) -> Path:
        db_path = (tmp_path / "trace.sqlite").resolve()
        conn = sqlite3.connect(db_path)
        cursor = conn.cursor()

        cursor.execute("""
            CREATE TABLE function_calls (
                type TEXT,
                function TEXT,
                classname TEXT,
                filename TEXT,
                line_number INTEGER,
                last_frame_address INTEGER,
                time_ns INTEGER,
                args BLOB
            )
        """)

        cursor.execute("""
            CREATE TABLE metadata (
                key TEXT PRIMARY KEY,
                value TEXT
            )
        """)

        cursor.execute("INSERT INTO metadata (key, value) VALUES ('language', 'javascript')")

        test_args = json.dumps([1, 2, 3])
        cursor.execute(
            """
            INSERT INTO function_calls (type, function, classname, filename, line_number, last_frame_address, time_ns, args)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?)
        """,
            ("call", "add", None, "/project/src/math.js", 10, 1, 1000000, test_args),
        )

        cursor.execute(
            """
            INSERT INTO function_calls (type, function, classname, filename, line_number, last_frame_address, time_ns, args)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?)
        """,
            ("call", "multiply", "Calculator", "/project/src/calc.js", 25, 2, 2000000, json.dumps([5, 10])),
        )

        conn.commit()
        conn.close()
        return db_path

    @pytest.fixture
    def trace_db_legacy_schema(self, tmp_path: Path) -> Path:
        db_path = (tmp_path / "legacy_trace.sqlite").resolve()
        conn = sqlite3.connect(db_path)
        cursor = conn.cursor()

        cursor.execute("""
            CREATE TABLE traces (
                id INTEGER PRIMARY KEY,
                call_id INTEGER,
                function TEXT,
                file TEXT,
                args TEXT,
                result TEXT,
                error TEXT,
                runtime_ns TEXT,
                timestamp INTEGER
            )
        """)

        cursor.execute(
            """
            INSERT INTO traces (call_id, function, file, args, result, error, runtime_ns, timestamp)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?)
        """,
            (1, "legacyFunc", "/old/path.js", json.dumps(["arg1"]), json.dumps("result"), "null", "5000", 1234567890),
        )

        conn.commit()
        conn.close()
        return db_path

    def test_parse_results_function_calls_schema(self, trace_db_with_function_calls: Path) -> None:
        traces = JavaScriptTracer.parse_results(trace_db_with_function_calls)

        assert len(traces) == 2

        add_trace = next(t for t in traces if t["function"] == "add")
        assert add_trace["type"] == "call"
        assert add_trace["filename"] == "/project/src/math.js"
        assert add_trace["line_number"] == 10
        assert add_trace["args"] == [1, 2, 3]
        assert add_trace["classname"] is None

        multiply_trace = next(t for t in traces if t["function"] == "multiply")
        assert multiply_trace["classname"] == "Calculator"
        assert multiply_trace["args"] == [5, 10]

    def test_parse_results_legacy_schema(self, trace_db_legacy_schema: Path) -> None:
        traces = JavaScriptTracer.parse_results(trace_db_legacy_schema)

        assert len(traces) == 1
        trace = traces[0]
        assert trace["function"] == "legacyFunc"
        assert trace["file"] == "/old/path.js"
        assert trace["args"] == ["arg1"]
        assert trace["result"] == "result"
        assert trace["runtime_ns"] == 5000

    def test_parse_results_nonexistent_file(self, tmp_path: Path) -> None:
        traces = JavaScriptTracer.parse_results((tmp_path / "nonexistent.sqlite").resolve())
        assert traces == []

    def test_parse_results_json_file(self, tmp_path: Path) -> None:
        json_path = (tmp_path / "trace.json").resolve()
        trace_data = [{"function": "jsonFunc", "args": [1, 2], "time_ns": 1000}]
        json_path.write_text(json.dumps(trace_data), encoding="utf-8")

        sqlite_path = (tmp_path / "trace.sqlite").resolve()
        traces = JavaScriptTracer.parse_results(sqlite_path)

        assert len(traces) == 1
        assert traces[0]["function"] == "jsonFunc"

    def test_get_traced_functions(self, trace_db_with_function_calls: Path) -> None:
        functions = JavaScriptTracer.get_traced_functions(trace_db_with_function_calls)

        assert len(functions) == 2

        func_names = {f.function_name for f in functions}
        assert func_names == {"add", "multiply"}

        add_func = next(f for f in functions if f.function_name == "add")
        assert add_func.file_name == "/project/src/math.js"
        assert add_func.class_name is None
        assert add_func.line_number == 10
        assert "math" in add_func.module_path

        multiply_func = next(f for f in functions if f.function_name == "multiply")
        assert multiply_func.class_name == "Calculator"


class TestJavaScriptReplayTestGeneration:

    def test_get_function_alias_simple(self) -> None:
        alias = get_function_alias("src/utils", "processData")
        assert alias == "src_utils_processData"

    def test_get_function_alias_with_class(self) -> None:
        alias = get_function_alias("src/calculator", "add", "Calculator")
        assert alias == "src_calculator_Calculator_add"

    def test_get_function_alias_special_chars(self) -> None:
        alias = get_function_alias("@scope/package/lib", "func")
        assert "_" in alias
        assert "func" in alias

    def test_create_javascript_replay_test_jest(self, tmp_path: Path) -> None:
        functions = [
            JavaScriptFunctionModule(
                function_name="add", file_name=(tmp_path / "math.js").resolve(), module_name="src/math"
            ),
            JavaScriptFunctionModule(
                function_name="multiply",
                file_name=(tmp_path / "calc.js").resolve(),
                module_name="src/calc",
                class_name="Calculator",
            ),
        ]

        content = create_javascript_replay_test(
            trace_file=str((tmp_path / "trace.sqlite").resolve()),
            functions=functions,
            max_run_count=50,
            framework="jest",
        )

        assert "// Auto-generated replay test by Codeflash" in content
        assert "require('codeflash/replay')" in content
        assert "describe('Replay: add'" in content
        assert "describe('Replay: Calculator.multiply'" in content
        assert "test.each" in content
        assert "50" in content

    def test_create_javascript_replay_test_vitest(self, tmp_path: Path) -> None:
        functions = [
            JavaScriptFunctionModule(
                function_name="process", file_name=(tmp_path / "data.js").resolve(), module_name="src/data"
            )
        ]

        content = create_javascript_replay_test(
            trace_file=str((tmp_path / "trace.sqlite").resolve()), functions=functions, framework="vitest"
        )

        assert "import { describe, test } from 'vitest'" in content
        assert "describe('Replay: process'" in content

    def test_create_javascript_replay_test_skips_constructors(self, tmp_path: Path) -> None:
        functions = [
            JavaScriptFunctionModule(
                function_name="constructor",
                file_name=(tmp_path / "class.js").resolve(),
                module_name="src/class",
                class_name="MyClass",
            ),
            JavaScriptFunctionModule(
                function_name="__init__", file_name=(tmp_path / "class.js").resolve(), module_name="src/class"
            ),
            JavaScriptFunctionModule(
                function_name="doWork", file_name=(tmp_path / "class.js").resolve(), module_name="src/class"
            ),
        ]

        content = create_javascript_replay_test(
            trace_file=str((tmp_path / "trace.sqlite").resolve()), functions=functions
        )

        assert "constructor" not in content.lower() or "Replay: constructor" not in content
        assert "__init__" not in content or "Replay: __init__" not in content
        assert "describe('Replay: doWork'" in content


class TestJavaScriptTracerCreateReplayTest:

    @pytest.fixture
    def trace_db(self, tmp_path: Path) -> Path:
        db_path = (tmp_path / "trace.sqlite").resolve()
        conn = sqlite3.connect(db_path)
        cursor = conn.cursor()

        cursor.execute("""
            CREATE TABLE function_calls (
                type TEXT,
                function TEXT,
                classname TEXT,
                filename TEXT,
                line_number INTEGER,
                last_frame_address INTEGER,
                time_ns INTEGER,
                args BLOB
            )
        """)

        cursor.execute(
            """
            INSERT INTO function_calls (type, function, classname, filename, line_number, last_frame_address, time_ns, args)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?)
        """,
            ("call", "fibonacci", None, "./src/math.js", 5, 1, 1000, json.dumps([10])),
        )

        conn.commit()
        conn.close()
        return db_path

    def test_create_replay_test_generates_file(self, trace_db: Path, tmp_path: Path) -> None:
        tracer = JavaScriptTracer(trace_db)
        output_path = (tmp_path / "tests" / "replay.test.js").resolve()

        result = tracer.create_replay_test(trace_db, output_path)

        assert result is not None
        assert output_path.exists()

        content = output_path.read_text(encoding="utf-8")
        assert "fibonacci" in content
        assert "describe" in content

    def test_create_replay_test_empty_db(self, tmp_path: Path) -> None:
        db_path = (tmp_path / "empty.sqlite").resolve()
        conn = sqlite3.connect(db_path)
        conn.execute("""
            CREATE TABLE function_calls (
                type TEXT, function TEXT, classname TEXT, filename TEXT,
                line_number INTEGER, last_frame_address INTEGER, time_ns INTEGER, args BLOB
            )
        """)
        conn.commit()
        conn.close()

        tracer = JavaScriptTracer(db_path)
        result = tracer.create_replay_test(db_path, (tmp_path / "test.js").resolve())

        assert result is None


@pytest.mark.skipif(os.name == "nt" or not node_available(), reason="Skipped on Windows or if Node.js not available")
class TestJavaScriptTracerE2E:

    @pytest.fixture
    def js_project(self, tmp_path: Path) -> Path:
        project_dir = (tmp_path / "js_project").resolve()
        project_dir.mkdir()

        src_dir = project_dir / "src"
        src_dir.mkdir()

        math_js = src_dir / "math.js"
        math_js.write_text(
            """
function add(a, b) {
    return a + b;
}

function multiply(a, b) {
    return a * b;
}

module.exports = { add, multiply };
""",
            encoding="utf-8",
        )

        main_js = project_dir / "main.js"
        main_js.write_text(
            """
const { add, multiply } = require('./src/math.js');

console.log('Running calculations...');
console.log('add(2, 3) =', add(2, 3));
console.log('add(10, 20) =', add(10, 20));
console.log('multiply(4, 5) =', multiply(4, 5));
""",
            encoding="utf-8",
        )

        package_json = project_dir / "package.json"
        package_json.write_text(
            json.dumps({"name": "test-project", "version": "1.0.0", "main": "main.js"}), encoding="utf-8"
        )

        return project_dir

    @pytest.fixture
    def trace_runner_path(self) -> Path:
        runner_path = Path(__file__).parent.parent.parent / "packages" / "codeflash" / "runtime" / "trace-runner.js"
        if not runner_path.exists():
            pytest.skip("trace-runner.js not found")
        return runner_path

    def test_trace_runner_help(self, trace_runner_path: Path) -> None:
        result = subprocess.run(
            ["node", str(trace_runner_path), "--help"], capture_output=True, text=True, timeout=30, check=False
        )

        assert "Usage:" in result.stdout or result.returncode == 0

    def test_trace_javascript_file(self, js_project: Path, trace_runner_path: Path, tmp_path: Path) -> None:
        trace_db = (tmp_path / "trace.sqlite").resolve()

        package_json = js_project / "package.json"
        package_json.write_text(
            json.dumps(
                {
                    "name": "test-project",
                    "version": "1.0.0",
                    "main": "main.js",
                    "dependencies": {
                        "@babel/core": "^7.24.0",
                        "@babel/register": "^7.24.0",
                        "better-sqlite3": "^11.0.0",
                    },
                }
            ),
            encoding="utf-8",
        )

        npm_install = subprocess.run(
            ["npm", "install", "--silent"], capture_output=True, text=True, timeout=120, cwd=js_project, check=False
        )

        if npm_install.returncode != 0:
            pytest.skip(f"npm install failed: {npm_install.stderr}")

        codeflash_runtime = trace_runner_path.parent
        node_modules = js_project / "node_modules"
        codeflash_pkg = node_modules / "codeflash"
        if codeflash_pkg.exists():
            shutil.rmtree(codeflash_pkg)
        codeflash_pkg.mkdir()
        runtime_dst = codeflash_pkg / "runtime"

        shutil.copytree(codeflash_runtime, runtime_dst)

        (codeflash_pkg / "package.json").write_text(
            json.dumps(
                {
                    "name": "codeflash",
                    "version": "1.0.0",
                    "main": "runtime/index.js",
                    "exports": {
                        ".": {"require": "./runtime/index.js"},
                        "./tracer": {"require": "./runtime/tracer.js"},
                        "./replay": {"require": "./runtime/replay.js"},
                        "./babel-tracer-plugin": {"require": "./runtime/babel-tracer-plugin.js"},
                    },
                }
            ),
            encoding="utf-8",
        )

        env = os.environ.copy()
        env["NODE_PATH"] = str(node_modules.resolve())

        result = subprocess.run(
            [
                "node",
                str(trace_runner_path),
                "--trace-db",
                str(trace_db),
                "--project-root",
                str(js_project),
                "--functions",
                json.dumps(["add", "multiply"]),
                str(js_project / "main.js"),
            ],
            capture_output=True,
            text=True,
            timeout=60,
            cwd=js_project,
            env=env,
            check=False,
        )

        if result.returncode != 0:
            print(f"STDOUT: {result.stdout}")
            print(f"STDERR: {result.stderr}")

        assert "add(2, 3) =" in result.stdout, f"Expected output not found. stderr: {result.stderr}"

        assert trace_db.exists(), "Trace database was not created"

        conn = sqlite3.connect(trace_db)
        cursor = conn.cursor()
        cursor.execute("SELECT COUNT(*) FROM function_calls WHERE type = 'call'")
        trace_count = cursor.fetchone()[0]
        conn.close()

        assert trace_count >= 2, f"Expected at least 2 traced calls, got {trace_count}"

    def test_tracer_runner_python_integration(self, js_project: Path, tmp_path: Path) -> None:
        from codeflash.languages.javascript.tracer_runner import (
            check_javascript_tracer_available,
            detect_test_framework,
        )

        assert check_javascript_tracer_available() is True

        framework = detect_test_framework(js_project, {})
        assert framework in ("jest", "vitest")

    def test_detect_jest_framework(self, tmp_path: Path) -> None:
        from codeflash.languages.javascript.tracer_runner import detect_test_framework

        (tmp_path / "jest.config.js").write_text("module.exports = {};", encoding="utf-8")
        framework = detect_test_framework(tmp_path, {})
        assert framework == "jest"

    def test_detect_vitest_framework(self, tmp_path: Path) -> None:
        from codeflash.languages.javascript.tracer_runner import detect_test_framework

        (tmp_path / "vitest.config.js").write_text("export default {};", encoding="utf-8")
        framework = detect_test_framework(tmp_path, {})
        assert framework == "vitest"

    def test_detect_framework_from_package_json(self, tmp_path: Path) -> None:
        from codeflash.languages.javascript.tracer_runner import detect_test_framework

        (tmp_path / "package.json").write_text(
            json.dumps({"scripts": {"test": "vitest run"}, "devDependencies": {"vitest": "^1.0.0"}}),
            encoding="utf-8",
        )
        framework = detect_test_framework(tmp_path, {})
        assert framework == "vitest"


class TestGetTracedFunctionsFromDb:

    def test_get_traced_functions_from_db(self, tmp_path: Path) -> None:
        db_path = (tmp_path / "trace.sqlite").resolve()
        conn = sqlite3.connect(db_path)
        cursor = conn.cursor()

        cursor.execute("""
            CREATE TABLE function_calls (
                type TEXT, function TEXT, classname TEXT, filename TEXT,
                line_number INTEGER, last_frame_address INTEGER, time_ns INTEGER, args BLOB
            )
        """)

        cursor.execute(
            "INSERT INTO function_calls VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
            ("call", "testFunc", None, "./src/test.js", 1, 1, 1000, "[]"),
        )

        conn.commit()
        conn.close()

        functions = get_traced_functions_from_db(db_path)

        assert len(functions) == 1
        assert functions[0].function_name == "testFunc"
        assert functions[0].module_name == "src/test"

    def test_get_traced_functions_nonexistent_file(self, tmp_path: Path) -> None:
        functions = get_traced_functions_from_db((tmp_path / "nonexistent.sqlite").resolve())
        assert functions == []
