"""End-to-end test of the update-toast flow.

Simulates a deploy: while the app is open with SW v3 active, we bump the SW
script to a new version (v4). The browser should detect the update, keep the
new SW in "waiting" state (NOT auto-activate), and show the "Update available"
toast. Clicking "Update now" should activate the new SW and reload.

We do this by serving a modified sw.js from a tiny local HTTP server on a
random port, so we don't touch the real files.
"""
import http.server
import os
import socket
import threading
import time

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.firefox.options import Options
from selenium.webdriver.firefox.service import Service

HERE = os.path.dirname(os.path.abspath(__file__))
GECKODRIVER = os.path.join(HERE, "geckodriver.exe")
HTML = os.path.join(HERE, "field-capture.html")
SW = os.path.join(HERE, "sw.js")

PORT = 8765


class Handler(http.server.SimpleHTTPRequestHandler):
    def log_message(self, *a):
        pass

    def do_GET(self):
        path = self.path.split("?")[0]
        print("SERVER GET:", path, flush=True)
        if path in ("/", "/field-capture.html"):
            with open(HTML, "rb") as f:
                body = f.read()
            self.send_response(200)
            self.send_header("Content-Type", "text/html; charset=utf-8")
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)
            return
        if self.path.split("?")[0] == "/sw.js":
            body = SW_CURRENT.encode()
            self.send_response(200)
            self.send_header("Content-Type", "application/javascript")
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)
            return
        # Everything else: serve from the local directory so the SW's
        # APP_SHELL (manifest.json, icons) can install successfully.
        try:
            fname = os.path.join(HERE, path.lstrip("/"))
            if os.path.isfile(fname):
                with open(fname, "rb") as f:
                    body = f.read()
                ctype = {
                    ".svg": "image/svg+xml",
                    ".png": "image/png",
                    ".json": "application/json",
                }.get(os.path.splitext(fname)[1].lower(), "application/octet-stream")
                self.send_response(200)
                self.send_header("Content-Type", ctype)
                self.send_header("Content-Length", str(len(body)))
                self.end_headers()
                self.wfile.write(body)
                return
        except Exception:
            pass
        self.send_response(404)
        self.send_header("Content-Length", "0")
        self.end_headers()


SW_CURRENT = open(SW, "r", encoding="utf-8").read()


def serve():
    srv = http.server.ThreadingHTTPServer(("127.0.0.1", PORT), Handler)
    srv.serve_forever()


def main():
    # Start the local server
    t = threading.Thread(target=serve, daemon=True)
    t.start()
    time.sleep(0.3)

    opts = Options()
    opts.headless = False
    svc = Service(executable_path=GECKODRIVER)
    d = webdriver.Firefox(options=opts, service=svc)
    try:
        # Clear any existing SW registration for our test origin
        d.get(f"http://127.0.0.1:{PORT}/")
        d.execute_script("""
            return navigator.serviceWorker.getRegistrations().then(rs =>
                Promise.all(rs.map(r => r.unregister()))
            );
        """)
        time.sleep(0.5)

        # Phase 1: first load installs SW v3
        d.get(f"http://127.0.0.1:{PORT}/field-capture.html?debug")
        time.sleep(3)
        # Direct registration attempt to see the error
        err = d.execute_script("""
            return navigator.serviceWorker.register('sw.js').then(
                r => 'registered:' + r.scope,
                e => 'ERROR: ' + e.message + ' | ' + e.name
            );
        """)
        print("Phase 1 - direct register:", err)
        active = d.execute_script(
            "return navigator.serviceWorker.getRegistration().then(r => r && r.active ? r.active.scriptURL : 'none')"
        )
        print("Phase 1 - active SW:", active)
        ctrl = d.execute_script(
            "return navigator.serviceWorker.controller ? navigator.serviceWorker.controller.scriptURL : 'none'"
        )
        print("Phase 1 - controller:", ctrl)

        # Phase 2: simulate deploy - bump SW version bytes. The version suffix
        # changes every release (e.g. 2026-09-06-14), so bump it generically
        # rather than matching a hard-coded old string.
        global SW_CURRENT
        import re as _re
        _m = _re.search(r"(SW_VERSION\s*=\s*'[^']+-)(\d+)'", SW_CURRENT)
        if _m:
            _new_ver = _m.group(1) + str(int(_m.group(2)) + 1) + "'"
            SW_CURRENT = SW_CURRENT[:_m.start()] + _new_ver + SW_CURRENT[_m.end():]
            print("Phase 2 - deployed SW v" + _new_ver.split("-")[-1].rstrip("'") + " (bytes changed)")
        else:
            # Fallback: append a marker so the bytes change.
            SW_CURRENT += "\n// update-test-bump\n"
            print("Phase 2 - deployed SW (bytes changed, fallback marker)")

        # Trigger update check from the page
        d.execute_script(
            "navigator.serviceWorker.getRegistration().then(r => r.update())"
        )
        time.sleep(3)

        # Check waiting state + toast
        waiting = d.execute_script(
            "return navigator.serviceWorker.getRegistration().then(r => r && r.waiting ? r.waiting.scriptURL : 'none')"
        )
        print("Phase 2 - waiting SW:", waiting)
        toast_visible = d.execute_script(
            "var t = document.getElementById('update-toast'); return t ? !t.classList.contains('hidden') : false"
        )
        print("Phase 2 - toast visible:", toast_visible)
        if toast_visible:
            print("  toast text:", d.find_element(By.ID, "update-toast").text[:120])

        # Phase 3: click "Update now"
        try:
            d.find_element(By.CSS_SELECTOR, '[data-action="apply-update"]').click()
            print("Phase 3 - clicked Update now")
        except Exception as e:
            print("Phase 3 - could not click:", e)
        time.sleep(4)

        active2 = d.execute_script(
            "return navigator.serviceWorker.getRegistration().then(r => r && r.active ? r.active.scriptURL : 'none')"
        )
        print("Phase 3 - active SW after update:", active2)
        # After reload the controller should be the new SW
        ctrl2 = d.execute_script(
            "return navigator.serviceWorker.controller ? navigator.serviceWorker.controller.scriptURL : 'none'"
        )
        print("Phase 3 - controller after reload:", ctrl2)
        # The toast should be gone (hidden) after applying
        toast_gone = d.execute_script(
            "var t = document.getElementById('update-toast'); return t ? t.classList.contains('hidden') : true"
        )
        print("Phase 3 - toast hidden after apply:", toast_gone)
    finally:
        d.quit()


if __name__ == "__main__":
    main()