from __future__ import annotations

import tempfile
import unittest
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import MagicMock

from phs.core.database import Database
from phs.modules.web_uptime import ProbeResult, WebTarget, WebUptimeModule, parse_statuses, parse_target_line


class FakeConfig:
    hostname = "srv.example.test"

    def get(self, section, option, fallback=""):
        return fallback

    def getint(self, section, option, fallback=0):
        values = {
            ("web_uptime", "failure_threshold"): 3,
            ("web_uptime", "repeat_alert_seconds"): 3600,
            ("web_uptime", "ssl_warning_days"): 21,
        }
        return values.get((section, option), fallback)

    def getbool(self, section, option, fallback=False):
        return fallback


class WebUptimeTests(unittest.TestCase):
    def setUp(self):
        self.temp = tempfile.TemporaryDirectory()
        self.database = Database(Path(self.temp.name) / "security.sqlite3")
        logger = MagicMock()
        logger.getChild.return_value = MagicMock()
        self.notifier = MagicMock()
        context = SimpleNamespace(
            config=FakeConfig(),
            database=self.database,
            notifier=self.notifier,
            logger=logger,
            tailer=MagicMock(),
        )
        self.module = WebUptimeModule(context)
        self.target = WebTarget(
            "Ejemplo",
            "https://example.test",
            parse_statuses("200-399"),
            timeout=10,
        )

    def tearDown(self):
        self.database.close()
        self.temp.cleanup()

    def result(self, ok: bool, code: int = 0, error: str = "") -> ProbeResult:
        return ProbeResult(
            self.target,
            ok,
            code,
            120,
            "192.0.2.20",
            self.target.url,
            0,
            error,
        )

    def test_parse_target_line(self):
        target = parse_target_line(
            "Tienda | https://example.com/salud | status=200,301 | contains=Inicio | timeout=8",
            parse_statuses("200-399"),
            12,
            True,
        )
        self.assertIsNotNone(target)
        assert target is not None
        self.assertEqual(target.name, "Tienda")
        self.assertEqual(target.timeout, 8)
        self.assertIn(301, target.expected_statuses)
        self.assertEqual(target.contains, "Inicio")

    def test_three_failures_mark_site_down_and_notify_once(self):
        self.assertEqual(self.module._process_result(self.result(False, error="timeout")), "pending")
        self.assertEqual(self.module._process_result(self.result(False, error="timeout")), "pending")
        self.assertEqual(self.module._process_result(self.result(False, error="timeout")), "down")
        row = self.database.query("SELECT status,consecutive_failures FROM web_status")[0]
        self.assertEqual(row["status"], "down")
        self.assertEqual(row["consecutive_failures"], 3)
        self.notifier.send.assert_called_once()
        self.assertIn("SITIO CAÍDO", self.notifier.send.call_args.args[0])

    def test_recovery_after_confirmed_outage_notifies(self):
        for _ in range(3):
            self.module._process_result(self.result(False, error="connection refused"))
        self.notifier.reset_mock()
        state = self.module._process_result(self.result(True, code=200))
        self.assertEqual(state, "up")
        row = self.database.query("SELECT status,consecutive_failures,outage_started FROM web_status")[0]
        self.assertEqual(row["status"], "up")
        self.assertEqual(row["consecutive_failures"], 0)
        self.assertEqual(row["outage_started"], 0)
        self.notifier.send.assert_called_once()
        self.assertIn("SITIO RECUPERADO", self.notifier.send.call_args.args[0])


    def test_suspended_cpanel_account_is_omitted_without_notification(self):
        suspended_target = WebTarget(
            "Suspendido",
            "https://suspended.example.test",
            parse_statuses("200-399"),
            timeout=10,
            cpanel_user="suspendido",
            account_suspended=True,
        )
        result = ProbeResult(
            suspended_target,
            False,
            0,
            10,
            "",
            suspended_target.url,
            0,
            "TimeoutError: timed out",
        )
        state = self.module._process_result(result)
        self.assertEqual(state, "suspended")
        row = self.database.query(
            "SELECT status,account_suspended,notification_suppressed FROM web_status WHERE url=?",
            (suspended_target.url,),
        )[0]
        self.assertEqual(row["status"], "suspended")
        self.assertEqual(row["account_suspended"], 1)
        self.assertEqual(row["notification_suppressed"], 1)
        self.assertEqual(self.database.scalar("SELECT COUNT(*) FROM web_outages", default=0), 0)
        self.notifier.send.assert_not_called()

    def test_outage_history_records_reason_and_closes_on_recovery(self):
        for _ in range(3):
            self.module._process_result(self.result(False, code=503, error="RuntimeError: HTTP 503 no permitido"))
        outage = self.database.query("SELECT * FROM web_outages")[0]
        self.assertEqual(outage["reason_category"], "HTTP 5xx")
        self.assertEqual(outage["ended_at"], 0)
        self.module._process_result(self.result(True, code=200))
        outage = self.database.query("SELECT * FROM web_outages")[0]
        self.assertGreater(outage["ended_at"], 0)
        self.assertGreaterEqual(outage["duration_seconds"], 0)


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