"""Tests for the tree-sitter parser and symbol extraction."""

import pytest

from tokennuke.parser.extractor import parse_file
from tokennuke.parser.symbols import Symbol, make_symbol_id, compute_content_hash


# --- Symbol utilities ---

class TestSymbolUtils:
    def test_make_symbol_id(self):
        sid = make_symbol_id('src/main.py', 'MyClass.login', 'method')
        assert sid == 'src/main.py::MyClass.login#method'

    def test_make_symbol_id_no_kind(self):
        sid = make_symbol_id('src/main.py', 'MyClass')
        assert sid == 'src/main.py::MyClass'

    def test_compute_content_hash(self):
        h1 = compute_content_hash(b'def foo(): pass')
        h2 = compute_content_hash(b'def foo(): pass')
        h3 = compute_content_hash(b'def bar(): pass')
        assert h1 == h2
        assert h1 != h3
        assert len(h1) == 64  # SHA-256 hex


# --- Python parsing ---

PYTHON_CODE = '''
"""Module docstring."""

MAX_RETRIES = 3
API_URL = "https://api.example.com"

class AuthService:
    """Handles authentication."""

    def login(self, username: str, password: str) -> bool:
        """Authenticate a user."""
        return self._verify(username, password)

    def _verify(self, username: str, password: str) -> bool:
        return username == "admin"

@staticmethod
def helper_func(x: int) -> int:
    """A standalone helper."""
    return x * 2
'''


class TestPythonParser:
    def test_extracts_class(self):
        symbols = parse_file(PYTHON_CODE, 'auth.py', 'python')
        classes = [s for s in symbols if s.kind == 'class']
        assert len(classes) == 1
        assert classes[0].name == 'AuthService'
        assert 'Handles authentication' in classes[0].docstring

    def test_extracts_methods(self):
        symbols = parse_file(PYTHON_CODE, 'auth.py', 'python')
        methods = [s for s in symbols if s.kind == 'method']
        assert len(methods) == 2
        names = {m.name for m in methods}
        assert 'login' in names
        assert '_verify' in names

    def test_extracts_functions(self):
        symbols = parse_file(PYTHON_CODE, 'auth.py', 'python')
        funcs = [s for s in symbols if s.kind == 'function']
        assert len(funcs) == 1
        assert funcs[0].name == 'helper_func'

    def test_extracts_constants(self):
        symbols = parse_file(PYTHON_CODE, 'auth.py', 'python')
        consts = [s for s in symbols if s.kind == 'constant']
        names = {c.name for c in consts}
        assert 'MAX_RETRIES' in names
        assert 'API_URL' in names

    def test_qualified_names(self):
        symbols = parse_file(PYTHON_CODE, 'auth.py', 'python')
        login = next(s for s in symbols if s.name == 'login')
        assert login.qualified_name == 'AuthService.login'

    def test_byte_offsets(self):
        symbols = parse_file(PYTHON_CODE, 'auth.py', 'python')
        for sym in symbols:
            assert sym.byte_offset >= 0
            assert sym.byte_length > 0
            # Verify O(1) retrieval works
            source_bytes = PYTHON_CODE.encode('utf-8')
            chunk = source_bytes[sym.byte_offset:sym.byte_offset + sym.byte_length]
            text = chunk.decode('utf-8')
            assert sym.name in text

    def test_docstrings(self):
        symbols = parse_file(PYTHON_CODE, 'auth.py', 'python')
        login = next(s for s in symbols if s.name == 'login')
        assert 'Authenticate a user' in login.docstring

    def test_signatures(self):
        symbols = parse_file(PYTHON_CODE, 'auth.py', 'python')
        login = next(s for s in symbols if s.name == 'login')
        assert 'username' in login.signature
        assert 'password' in login.signature

    def test_symbol_ids_unique(self):
        symbols = parse_file(PYTHON_CODE, 'auth.py', 'python')
        ids = [s.id for s in symbols]
        assert len(ids) == len(set(ids))

    def test_empty_file(self):
        symbols = parse_file('', 'empty.py', 'python')
        assert symbols == []

    def test_syntax_error_resilience(self):
        bad_code = 'def foo(:\n  pass\ndef bar():\n  return 1'
        symbols = parse_file(bad_code, 'bad.py', 'python')
        # Should extract what it can, not crash
        assert isinstance(symbols, list)

    def test_unknown_language(self):
        symbols = parse_file('hello', 'file.xyz', 'xyz')
        assert symbols == []


# --- JavaScript parsing ---

JS_CODE = '''
// Configuration manager
class ConfigManager {
    constructor(path) {
        this.path = path;
    }

    // Load configuration from file
    load() {
        return JSON.parse(fs.readFileSync(this.path));
    }
}

function validateConfig(config) {
    return config.version > 0;
}

const MAX_SIZE = 1024;
'''


class TestJavaScriptParser:
    def test_extracts_class(self):
        symbols = parse_file(JS_CODE, 'config.js', 'javascript')
        classes = [s for s in symbols if s.kind == 'class']
        assert len(classes) == 1
        assert classes[0].name == 'ConfigManager'

    def test_extracts_methods(self):
        symbols = parse_file(JS_CODE, 'config.js', 'javascript')
        methods = [s for s in symbols if s.kind == 'method']
        names = {m.name for m in methods}
        assert 'constructor' in names
        assert 'load' in names

    def test_extracts_function(self):
        symbols = parse_file(JS_CODE, 'config.js', 'javascript')
        funcs = [s for s in symbols if s.kind == 'function']
        assert any(f.name == 'validateConfig' for f in funcs)


# --- Go parsing ---

GO_CODE = '''
package main

// Server handles HTTP requests.
type Server struct {
    port int
    host string
}

// Start begins listening on the configured port.
func (s *Server) Start() error {
    return http.ListenAndServe(fmt.Sprintf("%s:%d", s.host, s.port), nil)
}

// NewServer creates a new Server with defaults.
func NewServer(port int) *Server {
    return &Server{port: port, host: "localhost"}
}
'''


class TestGoParser:
    def test_extracts_type(self):
        symbols = parse_file(GO_CODE, 'main.go', 'go')
        types = [s for s in symbols if s.kind == 'type']
        assert any(t.name == 'Server' for t in types)

    def test_extracts_method(self):
        symbols = parse_file(GO_CODE, 'main.go', 'go')
        methods = [s for s in symbols if s.kind == 'method']
        assert any(m.name == 'Start' for m in methods)

    def test_extracts_function(self):
        symbols = parse_file(GO_CODE, 'main.go', 'go')
        funcs = [s for s in symbols if s.kind == 'function']
        assert any(f.name == 'NewServer' for f in funcs)


# --- Rust parsing ---

RUST_CODE = '''
/// A configuration struct.
struct Config {
    port: u16,
    host: String,
}

/// Parse configuration from a TOML file.
fn parse_config(path: &str) -> Result<Config, Error> {
    let content = std::fs::read_to_string(path)?;
    toml::from_str(&content)
}

impl Config {
    /// Create a new Config with defaults.
    fn new() -> Self {
        Config { port: 8080, host: "localhost".into() }
    }
}
'''


class TestRustParser:
    def test_extracts_struct(self):
        symbols = parse_file(RUST_CODE, 'config.rs', 'rust')
        types = [s for s in symbols if s.kind == 'type']
        assert any(t.name == 'Config' for t in types)

    def test_extracts_function(self):
        symbols = parse_file(RUST_CODE, 'config.rs', 'rust')
        funcs = [s for s in symbols if s.kind == 'function']
        assert any(f.name == 'parse_config' for f in funcs)

    def test_extracts_impl_function(self):
        symbols = parse_file(RUST_CODE, 'config.rs', 'rust')
        # Functions inside impl blocks are extracted as functions
        # (impl_item doesn't expose a simple name field in tree-sitter-rust)
        funcs = [s for s in symbols if s.name == 'new']
        assert len(funcs) == 1
        assert funcs[0].kind in ('function', 'method')
