import os
import re
import sys
import unittest
from pathlib import Path
from unittest.mock import patch


ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
sys.path.insert(0, ROOT)

from PureWaf import PureWaf as core
from PureWaf import bypass
from PureWaf import bypass_data


class _FakeLogger:
    def __init__(self):
        self.messages = []

    def info(self, message):
        self.messages.append(str(message))

    def warning(self, message):
        self.messages.append(str(message))

    def error(self, message):
        self.messages.append(str(message))


class _NonTTYStream:
    def __init__(self):
        self._parts = []

    def write(self, text):
        self._parts.append(text)

    def flush(self):
        return None

    def isatty(self):
        return False

    def getvalue(self):
        return "".join(self._parts)


class PayloadGenerationImprovementTests(unittest.TestCase):
    def test_readfile_templates_remove_cp_mv(self):
        self.assertNotIn("mv {path} {path}.txt", bypass_data.READFILE_TEMPLATES)
        self.assertNotIn("cp {path} /tmp/flag.txt", bypass_data.READFILE_TEMPLATES)

    def test_generated_candidates_remove_cp_mv_payloads(self):
        options = bypass.BypassOptions(
            flagfile="/flag",
            read_env=False,
            reflect_shell=False,
            ip="127.0.0.1",
            port=8080,
            phpinfo=False,
            php_version=7.0,
        )
        payloads = bypass.generate_candidates(options)
        self.assertFalse(any("/tmp/flag.txt" in payload for payload in payloads))
        self.assertFalse(any("Y3AgL2ZsYWcgL3RtcC9mbGFnLnR4dA==" in payload for payload in payloads))
        self.assertFalse(any("bXYgL2ZsYWcgL2ZsYWcudHh0" in payload for payload in payloads))

    def test_root_discovery_diff_home_scope(self):
        options_root = bypass.BypassOptions(
            flagfile="/",
            read_env=False,
            reflect_shell=False,
            ip="127.0.0.1",
            port=8080,
            phpinfo=False,
            php_version=7.0,
        )
        root_payloads = bypass.generate_candidates(options_root)
        self.assertIn("diff / /home", root_payloads)

        options_flag = bypass.BypassOptions(
            flagfile="/etc/passwd",
            read_env=False,
            reflect_shell=False,
            ip="127.0.0.1",
            port=8080,
            phpinfo=False,
            php_version=7.0,
        )
        flag_payloads = bypass.generate_candidates(options_flag)
        self.assertNotIn("diff / /etc/passwd", flag_payloads)
        self.assertNotIn("diff / /home", flag_payloads)

    def test_remove_backtick_root_slash_payload(self):
        options = bypass.BypassOptions(
            flagfile="/",
            read_env=False,
            reflect_shell=False,
            ip="127.0.0.1",
            port=8080,
            phpinfo=False,
            php_version=7.0,
        )
        payloads = bypass.generate_candidates(options)
        self.assertNotIn("`/`", payloads)

    def test_remove_backtick_path_payload(self):
        options = bypass.BypassOptions(
            flagfile="/etc/passwd",
            read_env=False,
            reflect_shell=False,
            ip="127.0.0.1",
            port=8080,
            phpinfo=False,
            php_version=7.0,
        )
        payloads = bypass.generate_candidates(options)
        self.assertNotIn("`/etc/passwd`", payloads)

    def test_non_alnum_assert_post_payload_is_generated(self):
        options = bypass.BypassOptions(
            flagfile="/flag",
            read_env=False,
            reflect_shell=False,
            ip="127.0.0.1",
            port=8080,
            phpinfo=False,
            php_version=7.0,
        )
        payloads = bypass.generate_candidates(options)
        payload = bypass_data.NON_ALNUM_ASSERT_POST_PAYLOAD
        self.assertIn(payload, payloads)
        self.assertIsNone(re.search(r"[A-Za-z0-9]", payload))

    def test_assert_post_tip_emission(self):
        logger = _FakeLogger()
        core._emit_contextual_tips(logger, bypass_data.NON_ALNUM_ASSERT_POST_PAYLOAD, "/safe/")
        output = "\n".join(logger.messages)
        self.assertIn("TIPS: POST: 2=system('cat /flag.txt');", output)
        self.assertIn("TIPS: POST: 2=system('id');", output)

    def test_progressbar_non_tty_only_start_and_finish_lines(self):
        stream = _NonTTYStream()
        progress = bypass.ProgressBar(total=5, stream=stream, verbose=False)
        for idx in range(1, 6):
            progress.update(idx)
            if idx == 3:
                progress.mark_pass("ok")
        progress.finish()

        lines = [line for line in stream.getvalue().splitlines() if line.strip()]
        self.assertEqual(len(lines), 2)
        self.assertIn("0/5 passed:0", lines[0])
        self.assertIn("5/5 passed:1", lines[1])

    def test_register_update_notice_at_exit_once(self):
        previous = core._UPDATE_NOTICE_REGISTERED
        try:
            core._UPDATE_NOTICE_REGISTERED = False
            with patch("PureWaf.PureWaf.atexit.register") as mocked_register:
                core.register_update_notice_at_exit()
                core.register_update_notice_at_exit()
                self.assertEqual(mocked_register.call_count, 1)
        finally:
            core._UPDATE_NOTICE_REGISTERED = previous

    def test_notify_update_at_exit_calls_check(self):
        with patch("PureWaf.PureWaf._check_and_notify_update") as mocked_check:
            core._notify_update_at_exit()
            mocked_check.assert_called_once_with(core.version)

    def test_pyproject_has_no_console_script_entry(self):
        pyproject = Path(ROOT, "pyproject.toml").read_text(encoding="utf-8")
        self.assertNotIn("[project.scripts]", pyproject)


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