"""Tests for parser module — symbol extraction from source files."""

import tempfile
from pathlib import Path

import pytest

from tokennuke.parser.extractor import extract_symbols
from tokennuke.parser.languages import get_language_for_file, LANGUAGES
from tokennuke.parser.symbols import Symbol, CallEdge


# --- Language detection ---

class TestLanguageDetection:
    def test_python(self):
        assert get_language_for_file('main.py') == 'python'
        assert get_language_for_file('types.pyi') == 'python'

    def test_javascript(self):
        assert get_language_for_file('app.js') == 'javascript'
        assert get_language_for_file('component.jsx') == 'javascript'

    def test_typescript(self):
        assert get_language_for_file('server.ts') == 'typescript'
        assert get_language_for_file('page.tsx') == 'typescript'

    def test_go(self):
        assert get_language_for_file('main.go') == 'go'

    def test_rust(self):
        assert get_language_for_file('lib.rs') == 'rust'

    def test_java(self):
        assert get_language_for_file('Main.java') == 'java'

    def test_c(self):
        assert get_language_for_file('main.c') == 'c'
        assert get_language_for_file('header.h') == 'c'

    def test_cpp(self):
        assert get_language_for_file('main.cpp') == 'cpp'
        assert get_language_for_file('lib.hpp') == 'cpp'

    def test_csharp(self):
        assert get_language_for_file('Program.cs') == 'csharp'

    def test_ruby(self):
        assert get_language_for_file('app.rb') == 'ruby'

    def test_unknown(self):
        assert get_language_for_file('data.json') is None
        assert get_language_for_file('readme.md') is None

    def test_all_10_languages(self):
        assert len(LANGUAGES) == 10


# --- Python extraction ---

PYTHON_SOURCE = '''\
"""Module docstring."""

import os

CONSTANT = 42


def hello(name: str) -> str:
    """Say hello."""
    return f"Hello, {name}"


class Greeter:
    """A greeter class."""

    def __init__(self, prefix: str):
        self.prefix = prefix

    def greet(self, name: str) -> str:
        """Greet someone."""
        return hello(f"{self.prefix} {name}")


def main():
    g = Greeter("Dear")
    result = g.greet("World")
    print(result)
'''


class TestPythonExtraction:
    @pytest.fixture
    def symbols(self, tmp_path):
        p = tmp_path / 'example.py'
        p.write_text(PYTHON_SOURCE)
        return extract_symbols(str(p))

    def test_extracts_function(self, symbols):
        names = [s.name for s in symbols]
        assert 'hello' in names

    def test_extracts_class(self, symbols):
        names = [s.name for s in symbols]
        assert 'Greeter' in names

    def test_extracts_method(self, symbols):
        qnames = [s.qualified_name for s in symbols]
        assert 'Greeter.__init__' in qnames
        assert 'Greeter.greet' in qnames

    def test_function_kind(self, symbols):
        hello = next(s for s in symbols if s.name == 'hello')
        assert hello.kind == 'function'

    def test_class_kind(self, symbols):
        greeter = next(s for s in symbols if s.name == 'Greeter')
        assert greeter.kind == 'class'

    def test_method_kind(self, symbols):
        init = next(s for s in symbols if s.name == '__init__')
        assert init.kind == 'function'  # function_definition

    def test_docstring(self, symbols):
        hello = next(s for s in symbols if s.name == 'hello')
        assert 'Say hello' in hello.docstring

    def test_signature(self, symbols):
        hello = next(s for s in symbols if s.name == 'hello')
        assert 'name: str' in hello.signature
        assert 'str' in hello.signature

    def test_line_numbers(self, symbols):
        hello = next(s for s in symbols if s.name == 'hello')
        assert hello.line == 8
        assert hello.end_line == 10

    def test_byte_offset(self, symbols):
        hello = next(s for s in symbols if s.name == 'hello')
        assert hello.byte_offset > 0
        assert hello.byte_length > 0

    def test_call_edges(self, symbols):
        greet = next(s for s in symbols if s.name == 'greet')
        callee_names = [c.callee_name for c in greet.calls]
        assert 'hello' in callee_names

    def test_main_calls(self, symbols):
        main = next(s for s in symbols if s.name == 'main')
        callee_names = [c.callee_name for c in main.calls]
        assert any('Greeter' in c for c in callee_names)

    def test_parent_name(self, symbols):
        init = next(s for s in symbols if s.name == '__init__')
        assert init.parent_name == 'Greeter'

    def test_symbol_count(self, symbols):
        # hello, Greeter, __init__, greet, main = 5
        assert len(symbols) >= 5


# --- JavaScript extraction ---

JS_SOURCE = '''\
function add(a, b) {
    return a + b;
}

class Calculator {
    constructor(initial) {
        this.value = initial;
    }

    add(n) {
        this.value = add(this.value, n);
        return this;
    }
}

const multiply = (a, b) => a * b;
'''


class TestJavaScriptExtraction:
    @pytest.fixture
    def symbols(self, tmp_path):
        p = tmp_path / 'calc.js'
        p.write_text(JS_SOURCE)
        return extract_symbols(str(p))

    def test_extracts_function(self, symbols):
        names = [s.name for s in symbols]
        assert 'add' in names

    def test_extracts_class(self, symbols):
        names = [s.name for s in symbols]
        assert 'Calculator' in names

    def test_extracts_arrow_function(self, symbols):
        names = [s.name for s in symbols]
        # Arrow functions inside const may or may not be extracted as named symbols
        # depending on the grammar — at minimum we get the function and class
        assert len(symbols) >= 2


# --- Go extraction ---

GO_SOURCE = '''\
package main

import "fmt"

func Hello(name string) string {
    return fmt.Sprintf("Hello, %s", name)
}

type Greeter struct {
    Prefix string
}

func (g *Greeter) Greet(name string) string {
    return Hello(g.Prefix + " " + name)
}
'''


class TestGoExtraction:
    @pytest.fixture
    def symbols(self, tmp_path):
        p = tmp_path / 'main.go'
        p.write_text(GO_SOURCE)
        return extract_symbols(str(p))

    def test_extracts_function(self, symbols):
        names = [s.name for s in symbols]
        assert 'Hello' in names

    def test_extracts_struct(self, symbols):
        kinds = {s.name: s.kind for s in symbols}
        # Greeter should be found (either as class/struct from type_declaration or struct_specifier)
        assert any('Greeter' in s.name for s in symbols) or len(symbols) >= 2


# --- Rust extraction ---

RUST_SOURCE = '''\
fn add(a: i32, b: i32) -> i32 {
    a + b
}

struct Point {
    x: f64,
    y: f64,
}

impl Point {
    fn distance(&self) -> f64 {
        (self.x * self.x + self.y * self.y).sqrt()
    }
}
'''


class TestRustExtraction:
    @pytest.fixture
    def symbols(self, tmp_path):
        p = tmp_path / 'lib.rs'
        p.write_text(RUST_SOURCE)
        return extract_symbols(str(p))

    def test_extracts_function(self, symbols):
        names = [s.name for s in symbols]
        assert 'add' in names

    def test_extracts_struct(self, symbols):
        structs = [s for s in symbols if s.kind in ('class', 'struct')]
        assert any('Point' in s.name for s in structs)


# --- Edge cases ---

class TestEdgeCases:
    def test_empty_file(self, tmp_path):
        p = tmp_path / 'empty.py'
        p.write_text('')
        assert extract_symbols(str(p)) == []

    def test_nonexistent_file(self):
        assert extract_symbols('/nonexistent/file.py') == []

    def test_binary_file(self, tmp_path):
        p = tmp_path / 'binary.py'
        p.write_bytes(b'\x7fELF' + b'\x00' * 100)
        assert extract_symbols(str(p)) == []

    def test_unknown_extension(self, tmp_path):
        p = tmp_path / 'data.json'
        p.write_text('{"key": "value"}')
        assert extract_symbols(str(p)) == []

    def test_syntax_error_doesnt_crash(self, tmp_path):
        p = tmp_path / 'broken.py'
        p.write_text('def broken(\n    pass')
        # Should not raise, may return partial results
        result = extract_symbols(str(p))
        assert isinstance(result, list)
