"""Tests for scripts/setup_env.py `--init` support.

Covers the only non-trivial piece: `_write_env_vars` must replace existing
keys in place, append new ones, preserve unrelated lines, and lock perms to
0600 (the file holds a Canvas access token). No network — init_env()'s
validation call is not exercised here.
"""

import os
import sys
import tempfile
from unittest.mock import MagicMock, patch

SCRIPTS_DIR = os.path.join(os.path.dirname(__file__), "..", "scripts")
sys.path.insert(0, SCRIPTS_DIR)

import setup_env


def _read(path):
    """Read a file through a context manager.

    The previous inline ``open(p).read()`` calls leaked their handles (no
    close), which surfaced as ResourceWarning / PytestUnraisableExceptionWarning
    under ``-W error::Warning`` when the GC finalized the still-open _io
    objects. setup_env.py itself uses context managers throughout — the leak
    was in this test file.
    """
    with open(path) as f:
        return f.read()


def test_write_env_vars_replaces_preserves_appends_and_chmods():
    with tempfile.TemporaryDirectory() as d:
        p = os.path.join(d, ".env")
        with open(p, "w") as f:
            f.write("CANVAS_TOKEN=old\nOTHER_KEY=keepme\n")

        setup_env._write_env_vars(
            {"CANVAS_TOKEN": "new", "CANVAS_DOMAIN": "canvas.asu.edu"}, env_path=p
        )

        content = _read(p)
        assert "CANVAS_TOKEN=new" in content            # replaced
        assert content.count("CANVAS_TOKEN=") == 1       # not duplicated
        assert "OTHER_KEY=keepme" in content             # unrelated line preserved
        assert "CANVAS_DOMAIN=canvas.asu.edu" in content  # new key appended
        assert oct(os.stat(p).st_mode)[-3:] == "600"     # token-file perms


def test_write_env_vars_creates_file_when_absent():
    with tempfile.TemporaryDirectory() as d:
        p = os.path.join(d, ".env")
        setup_env._write_env_vars({"CANVAS_TOKEN": "abc"}, env_path=p)
        assert _read(p) == "CANVAS_TOKEN=abc\n"
        assert oct(os.stat(p).st_mode)[-3:] == "600"


def test_write_env_vars_collapses_duplicate_keys():
    # A malformed .env with a repeated key must collapse to one line — otherwise
    # a stale duplicate shadows the new value under dotenv's last-wins load.
    with tempfile.TemporaryDirectory() as d:
        p = os.path.join(d, ".env")
        with open(p, "w") as f:
            f.write("CANVAS_TOKEN=old1\nCANVAS_TOKEN=old2\nOTHER=keep\n")

        setup_env._write_env_vars({"CANVAS_TOKEN": "new"}, env_path=p)

        content = _read(p)
        assert content.count("CANVAS_TOKEN=") == 1   # collapsed, not two lines
        assert "CANVAS_TOKEN=new" in content
        assert "old1" not in content and "old2" not in content
        assert "OTHER=keep" in content               # unrelated line preserved


def test_init_env_rejects_non_json_200_without_writing():
    # A 200 response with a non-JSON body (captive portal / proxy HTML) must
    # fail cleanly, not traceback, and must NOT persist the token.
    fake_resp = MagicMock(status_code=200)
    fake_resp.json.side_effect = ValueError("not json")
    with patch.object(setup_env.requests, "get", return_value=fake_resp), \
         patch.object(setup_env.getpass, "getpass", return_value="tok"), \
         patch("builtins.input", return_value="canvas.asu.edu"), \
         patch.object(setup_env, "_write_env_vars") as mock_write:
        result = setup_env.init_env()

    assert result is False
    mock_write.assert_not_called()               # token never written


def test_normalize_domain_accepts_and_normalizes_allowed_hosts():
    assert setup_env.normalize_domain("canvas.asu.edu") == "canvas.asu.edu"
    assert setup_env.normalize_domain("https://asu.instructure.com/") == "asu.instructure.com"
    assert setup_env.normalize_domain("  ASU-DEV.instructure.com  ") == "asu-dev.instructure.com"


def test_normalize_domain_rejects_off_allowlist():
    # Exact-membership check means every off-allowlist form rejects: typos,
    # subdomain-suffix attacks, embedded userinfo (@host), ports, IDN
    # homographs, empty/degenerate hosts, and trailing-dot FQDNs.
    bad_inputs = (
        "canvas.evil.com",
        "canvas.asu.edu.evil.com",           # subdomain-suffix
        "canvas.asu.edu@evil.com",           # userinfo, no scheme
        "https://canvas.asu.edu@evil.com/x",  # userinfo, full URL
        "canvas.asu.edu:80",                 # port
        "сanvas.asu.edu",               # Cyrillic 'с' homograph
        "https:///canvas.asu.edu",           # empty host
        "canvas.asu.edu.",                   # trailing-dot FQDN
        "localhost",
        "",
    )
    for bad in bad_inputs:
        try:
            setup_env.normalize_domain(bad)
            assert False, f"expected rejection for {bad!r}"
        except ValueError:
            pass


def test_test_connection_off_allowlist_domain_sends_no_token():
    with patch.object(setup_env, "get_env",
                      return_value={"token": "tok", "domain": "canvas.evil.com", "course_id": None}), \
         patch.object(setup_env.requests, "get") as mock_get:
        result = setup_env.test_connection()
    assert result is False
    mock_get.assert_not_called()          # token never left the process


def test_verify_course_off_allowlist_domain_sends_no_token():
    with patch.object(setup_env, "get_env",
                      return_value={"token": "tok", "domain": "canvas.evil.com", "course_id": None}), \
         patch.object(setup_env.requests, "get") as mock_get:
        result = setup_env.verify_course("123")
    assert result is False
    mock_get.assert_not_called()


def test_init_env_off_allowlist_domain_sends_no_token_and_writes_nothing():
    with patch("builtins.input", return_value="canvas.evil.com"), \
         patch.object(setup_env.getpass, "getpass", return_value="tok"), \
         patch.object(setup_env.requests, "get") as mock_get, \
         patch.object(setup_env, "_write_env_vars") as mock_write:
        result = setup_env.init_env()
    assert result is False
    mock_get.assert_not_called()          # token never sent
    mock_write.assert_not_called()        # token never persisted


def test_list_courses_off_allowlist_domain_sends_no_token():
    off = {"token": "tok", "domain": "canvas.evil.com", "course_id": None}
    with patch.object(setup_env, "get_env", return_value=off), \
         patch.object(setup_env.canvas_api, "paginated_get") as mock_pg, \
         patch.object(setup_env.requests, "get") as mock_get:
        result = setup_env.list_courses()
    assert result is False
    mock_pg.assert_not_called()           # never reached the network
    mock_get.assert_not_called()


def test_sync_registry_off_allowlist_domain_sends_no_token():
    off = {"token": "tok", "domain": "canvas.evil.com", "course_id": None}
    with patch.object(setup_env, "get_env", return_value=off), \
         patch.object(setup_env.canvas_api, "paginated_get") as mock_pg, \
         patch.object(setup_env.requests, "get") as mock_get:
        result = setup_env.sync_registry()
    assert result is False
    mock_pg.assert_not_called()
    mock_get.assert_not_called()


def test_migrate_off_allowlist_domain_sends_no_token():
    # course_id set so migrate gets past the "nothing to migrate" early return
    # and reaches the domain guard.
    off = {"token": "tok", "domain": "canvas.evil.com", "course_id": "123"}
    with patch.object(setup_env, "get_env", return_value=off), \
         patch.object(setup_env.canvas_api, "paginated_get") as mock_pg, \
         patch.object(setup_env.requests, "get") as mock_get:
        result = setup_env.migrate()
    assert result is False
    mock_pg.assert_not_called()           # bulk fetch never sent
    mock_get.assert_not_called()          # single-course URL never sent


if __name__ == "__main__":
    test_write_env_vars_replaces_preserves_appends_and_chmods()
    test_write_env_vars_creates_file_when_absent()
    test_write_env_vars_collapses_duplicate_keys()
    test_init_env_rejects_non_json_200_without_writing()
    test_normalize_domain_accepts_and_normalizes_allowed_hosts()
    test_normalize_domain_rejects_off_allowlist()
    test_test_connection_off_allowlist_domain_sends_no_token()
    test_verify_course_off_allowlist_domain_sends_no_token()
    test_init_env_off_allowlist_domain_sends_no_token_and_writes_nothing()
    test_list_courses_off_allowlist_domain_sends_no_token()
    test_sync_registry_off_allowlist_domain_sends_no_token()
    test_migrate_off_allowlist_domain_sends_no_token()
    print("ok")
