"""Coverage boost part 3 — target low-coverage modules: web internals, llm, telegram, ws, agents."""
import asyncio
import json
import os
import sys
import tempfile
import unittest
from pathlib import Path
from unittest.mock import MagicMock, patch, PropertyMock

sys.path.insert(0, str(Path(__file__).parent.parent))


class TestWebInternals(unittest.TestCase):
    """Test web.py internal methods directly."""

    def test_security_headers(self):
        from salmalm.web import WebHandler
        handler = MagicMock(spec=WebHandler)
        handler.send_header = MagicMock()
        # Call the actual method
        if hasattr(WebHandler, '_security_headers'):
            WebHandler._security_headers(handler)
            self.assertTrue(handler.send_header.called)

    def test_needs_onboarding(self):
        from salmalm.web import WebHandler
        handler = MagicMock(spec=WebHandler)
        if hasattr(WebHandler, '_needs_onboarding'):
            with patch('salmalm.web.vault') as mv:
                mv.is_unlocked = True
                mv.get.return_value = None
                result = WebHandler._needs_onboarding(handler)
                self.assertIsInstance(result, bool)

    def test_parse_json_body(self):
        from salmalm.web import WebHandler
        handler = MagicMock(spec=WebHandler)
        handler.headers = {'content-length': '13', 'content-type': 'application/json'}
        handler.rfile = MagicMock()
        handler.rfile.read.return_value = b'{"key":"val"}'
        if hasattr(WebHandler, '_parse_json_body'):
            result = WebHandler._parse_json_body(handler)
            self.assertIsInstance(result, dict)

    def test_send_json(self):
        from salmalm.web import WebHandler
        handler = MagicMock(spec=WebHandler)
        handler.wfile = MagicMock()
        if hasattr(WebHandler, '_send_json'):
            WebHandler._send_json(handler, {'ok': True})
            self.assertTrue(handler.send_response.called or handler.wfile.write.called)


class TestLLMInternals(unittest.TestCase):
    """Test LLM internal utilities."""

    def test_build_headers(self):
        from salmalm.core.llm import call_llm
        # Just test import and that the function exists
        self.assertTrue(callable(call_llm))

    def test_track_usage(self):
        from salmalm.core.llm import track_usage
        # Should not raise
        track_usage('test/model', 100, 50)

    def test_track_usage_zero(self):
        from salmalm.core.llm import track_usage
        track_usage('test/model2', 0, 0)

    @patch('salmalm.core.llm.urllib.request.urlopen')
    def test_call_anthropic_mock(self, mock_urlopen):
        from salmalm.core.llm import call_llm
        mock_resp = MagicMock()
        mock_resp.read.return_value = json.dumps({
            'content': [{'type': 'text', 'text': 'Hello!'}],
            'usage': {'input_tokens': 10, 'output_tokens': 5}
        }).encode()
        mock_resp.__enter__ = lambda s: s
        mock_resp.__exit__ = MagicMock(return_value=False)
        mock_urlopen.return_value = mock_resp
        loop = asyncio.new_event_loop()
        try:
            result = loop.run_until_complete(
                call_llm([{'role': 'user', 'content': 'hi'}], model='anthropic/claude-3-haiku')
            )
        except Exception:
            pass
        finally:
            loop.close()

    @patch('salmalm.core.llm.urllib.request.urlopen')
    def test_call_google_mock(self, mock_urlopen):
        from salmalm.core.llm import call_llm
        mock_resp = MagicMock()
        mock_resp.read.return_value = json.dumps({
            'candidates': [{'content': {'parts': [{'text': 'Hello!'}]}}],
            'usageMetadata': {'promptTokenCount': 10, 'candidatesTokenCount': 5}
        }).encode()
        mock_resp.__enter__ = lambda s: s
        mock_resp.__exit__ = MagicMock(return_value=False)
        mock_urlopen.return_value = mock_resp
        loop = asyncio.new_event_loop()
        try:
            result = loop.run_until_complete(
                call_llm([{'role': 'user', 'content': 'hi'}], model='google/gemini-pro')
            )
        except Exception:
            pass
        finally:
            loop.close()


class TestAgentsInternals(unittest.TestCase):
    """Test agents module internals."""

    def test_skill_loader_scan(self):
        from salmalm.features.agents import SkillLoader
        sl = SkillLoader()
        skills = sl.scan()
        self.assertIsInstance(skills, list)

    def test_skill_loader_match(self):
        from salmalm.features.agents import SkillLoader
        sl = SkillLoader()
        result = sl.match("test query")
        # May return None or a skill
        self.assertTrue(result is None or isinstance(result, dict))

    def test_sub_agent_init(self):
        from salmalm.features.agents import SubAgent
        sa = SubAgent.__new__(SubAgent)
        self.assertIsNotNone(sa)


class TestWSInternals(unittest.TestCase):
    """Test WebSocket internals."""

    def test_ws_client_init(self):
        from salmalm.web.ws import WSClient
        # Can't create without reader/writer, just verify class exists
        self.assertTrue(hasattr(WSClient, '__init__'))

    def test_ws_magic(self):
        from salmalm.web.ws import WS_MAGIC
        self.assertIsInstance(WS_MAGIC, (str, bytes))

    def test_opcodes(self):
        from salmalm.web.ws import OP_TEXT, OP_BIN, OP_CLOSE, OP_PING, OP_PONG
        self.assertEqual(OP_TEXT, 0x1)
        self.assertEqual(OP_BIN, 0x2)
        self.assertEqual(OP_CLOSE, 0x8)
        self.assertEqual(OP_PING, 0x9)
        self.assertEqual(OP_PONG, 0xA)


class TestMCPInternals(unittest.TestCase):
    """Test MCP internals."""

    def test_mcp_server_init(self):
        from salmalm.features.mcp import MCPServer
        srv = MCPServer.__new__(MCPServer)
        self.assertIsNotNone(srv)

    def test_mcp_manager_add_remove(self):
        from salmalm.features.mcp import MCPManager
        mgr = MCPManager()
        # Add a dummy server config
        try:
            mgr.add_server('test_srv', {'command': 'echo hello'})
            servers = mgr.list_servers()
            mgr.remove_server('test_srv')
        except Exception:
            pass  # May fail without actual MCP server

    def test_mcp_manager_load_config(self):
        from salmalm.features.mcp import MCPManager
        mgr = MCPManager()
        mgr.load_config()  # Should not raise


class TestNodesInternals(unittest.TestCase):
    """Test nodes internals."""

    def test_node_load_config(self):
        from salmalm.features.nodes import NodeManager
        mgr = NodeManager()
        mgr.load_config()

    def test_node_save_config(self):
        from salmalm.features.nodes import NodeManager
        mgr = NodeManager()
        mgr.save_config()

    def test_node_get_nonexistent(self):
        from salmalm.features.nodes import NodeManager
        mgr = NodeManager()
        node = mgr.get_node('nonexistent_node_xyz')
        self.assertIsNone(node)

    def test_http_node_init(self):
        from salmalm.features.nodes import HTTPNode
        node = HTTPNode.__new__(HTTPNode)
        self.assertIsNotNone(node)


class TestBrowserInternals(unittest.TestCase):
    """Test browser internals."""

    def test_cdp_connection_init(self):
        from salmalm.utils.browser import CDPConnection
        cdp = CDPConnection.__new__(CDPConnection)
        self.assertIsNotNone(cdp)

    def test_browser_get_tabs_not_connected(self):
        import asyncio
        from salmalm.utils.browser import BrowserController
        bc = BrowserController()
        loop = asyncio.new_event_loop()
        try:
            tabs = loop.run_until_complete(bc.get_tabs())
            self.assertIsInstance(tabs, list)
        except Exception:
            pass
        finally:
            loop.close()

    def test_browser_get_text_not_connected(self):
        import asyncio
        from salmalm.utils.browser import BrowserController
        bc = BrowserController()
        loop = asyncio.new_event_loop()
        try:
            text = loop.run_until_complete(bc.get_text())
            self.assertIsInstance(text, str)
        except Exception:
            pass
        finally:
            loop.close()


class TestToolHandlersEdgeCases(unittest.TestCase):
    """Test tool handler edge cases."""

    def _ws_path(self, name):
        from salmalm.constants import WORKSPACE_DIR
        p = WORKSPACE_DIR / '_test_tmp'
        p.mkdir(exist_ok=True)
        return str(p / name)

    def test_read_nonexistent(self):
        from salmalm.tools.tool_handlers import execute_tool
        result = execute_tool('read_file', {'path': '/nonexistent_xyz.txt'})
        self.assertTrue('error' in result.lower() or 'denied' in result.lower())

    def test_write_empty(self):
        from salmalm.tools.tool_handlers import execute_tool
        path = self._ws_path('empty.txt')
        result = execute_tool('write_file', {'path': path, 'content': ''})
        self.assertIsInstance(result, str)
        if os.path.exists(path):
            os.unlink(path)

    @patch.dict(os.environ, {"SALMALM_PYTHON_EVAL": "1"})
    def test_python_eval_error(self):
        from salmalm.tools.tool_handlers import execute_tool
        result = execute_tool('python_eval', {'code': 'raise ValueError("test")'})
        self.assertIsInstance(result, str)

    @patch.dict(os.environ, {"SALMALM_PYTHON_EVAL": "1"})
    def test_python_eval_timeout(self):
        from salmalm.tools.tool_handlers import execute_tool
        result = execute_tool('python_eval', {'code': 'import time; time.sleep(0.1); print("ok")'})
        self.assertIn('ok', result)

    def test_hash_md5(self):
        from salmalm.tools.tool_handlers import execute_tool
        result = execute_tool('hash_text', {'text': 'hello', 'algorithm': 'md5'})
        self.assertIn('5d41402', result)

    def test_hash_sha1(self):
        from salmalm.tools.tool_handlers import execute_tool
        result = execute_tool('hash_text', {'text': 'hello', 'algorithm': 'sha1'})
        self.assertIn('aaf4c61', result)

    def test_json_query_nested(self):
        from salmalm.tools.tool_handlers import execute_tool
        result = execute_tool('json_query', {
            'data': '{"a": {"b": {"c": [1,2,3]}}}',
            'query': 'a.b.c'
        })
        self.assertIn('1', result)

    def test_json_query_invalid(self):
        from salmalm.tools.tool_handlers import execute_tool
        result = execute_tool('json_query', {'data': 'not json', 'query': 'a'})
        self.assertIsInstance(result, str)

    def test_regex_no_match(self):
        from salmalm.tools.tool_handlers import execute_tool
        result = execute_tool('regex_test', {'pattern': r'\d+', 'text': 'no numbers here'})
        self.assertIsInstance(result, str)

    def test_regex_invalid(self):
        from salmalm.tools.tool_handlers import execute_tool
        result = execute_tool('regex_test', {'pattern': r'[invalid', 'text': 'test'})
        self.assertIsInstance(result, str)

    def test_edit_nonexistent(self):
        from salmalm.tools.tool_handlers import execute_tool
        result = execute_tool('edit_file', {
            'path': '/nonexistent_xyz.txt',
            'old_text': 'a',
            'new_text': 'b'
        })
        self.assertIsInstance(result, str)

    def test_diff_nonexistent(self):
        from salmalm.tools.tool_handlers import execute_tool
        result = execute_tool('diff_files', {
            'file1': '/nonexistent1.txt',
            'file2': '/nonexistent2.txt'
        })
        self.assertIsInstance(result, str)

    def test_clipboard_read(self):
        from salmalm.tools.tool_handlers import execute_tool
        result = execute_tool('clipboard', {'action': 'read'})
        self.assertIsInstance(result, str)

    def test_system_monitor_extended(self):
        from salmalm.tools.tool_handlers import execute_tool
        result = execute_tool('system_monitor', {'detail': True})
        self.assertIsInstance(result, str)


class TestCoreInternals(unittest.TestCase):
    """Test core module internals."""

    def test_session_messages(self):
        from salmalm.core import get_session
        s = get_session('clear_test3')
        initial = len(s.messages)
        s.add_user('msg1')
        s.add_user('msg2')
        self.assertEqual(len(s.messages), initial + 2)

    def test_usage_report(self):
        from salmalm.core import get_usage_report
        report = get_usage_report()
        self.assertIn('total_input', report)
        self.assertIn('total_output', report)
        self.assertIn('total_cost', report)

    def test_multiple_sessions(self):
        from salmalm.core import get_session
        s1 = get_session('multi_1')
        s2 = get_session('multi_2')
        s1.add_user('hello 1')
        s2.add_user('hello 2')
        self.assertNotEqual(id(s1), id(s2))


class TestRAGInternals(unittest.TestCase):
    """Test RAG internals."""

    def test_tokenize_korean(self):
        from salmalm.features.rag import RAGEngine
        tokens = RAGEngine._tokenize("안녕하세요 세계")
        self.assertTrue(len(tokens) >= 1)

    def test_tokenize_empty(self):
        from salmalm.features.rag import RAGEngine
        tokens = RAGEngine._tokenize("")
        self.assertEqual(len(tokens), 0)

    def test_tokenize_special_chars(self):
        from salmalm.features.rag import RAGEngine
        tokens = RAGEngine._tokenize("hello! @world #test $123")
        self.assertTrue(len(tokens) >= 1)


class TestStabilityInternals(unittest.TestCase):
    """Test stability internals."""

    def test_auto_recover(self):
        import asyncio
        from salmalm.features.stability import HealthMonitor
        hm = HealthMonitor()
        # auto_recover is async — run it properly to avoid RuntimeWarning
        asyncio.run(hm.auto_recover())

    def test_startup_selftest(self):
        from salmalm.features.stability import HealthMonitor
        hm = HealthMonitor()
        result = hm.startup_selftest()
        self.assertIsInstance(result, (dict, list, str, bool))


if __name__ == '__main__':
    unittest.main()
