#!/usr/bin/env python3
"""ERP Auth Module API Integration Test Suite"""
import urllib.request
import json
import time
import sys
import os

BASE = "http://localhost:3000"

def api(method, path, data=None, tok=None):
    url = BASE + path
    headers = {"Content-Type": "application/json"}
    if tok:
        headers["Authorization"] = "Bearer " + tok
    body = json.dumps(data).encode("utf-8") if data else None
    req = urllib.request.Request(url, data=body, headers=headers, method=method)
    start = int(time.time() * 1000)
    try:
        with urllib.request.urlopen(req) as resp:
            elapsed = int(time.time() * 1000) - start
            return resp.status, json.loads(resp.read().decode("utf-8")), elapsed
    except urllib.error.HTTPError as e:
        elapsed = int(time.time() * 1000) - start
        try:
            rbody = json.loads(e.read().decode("utf-8"))
        except Exception:
            rbody = {"raw": str(e)}
        return e.code, rbody, elapsed
    except Exception as e:
        elapsed = int(time.time() * 1000) - start
        return 0, {"error": str(e)}, elapsed

results = []
created_resources = {"users": [], "roles": [], "organizations": [], "departments": [], "positions": []}

def log(tid, name, code, ms, detail, expect_pass=True):
    if expect_pass:
        status = "PASS" if 200 <= code < 300 else "FAIL"
    else:
        status = "PASS" if code >= 400 else "FAIL"
    line = f"  [{status}] {tid} {name}: HTTP {code}, {ms}ms | {detail[:120]}"
    print(line)
    results.append({"id": tid, "name": name, "status": status, "code": code, "ms": ms, "detail": detail})

def login():
    code, body, ms = api("POST", "/api/v1/auth/login", {
        "username": "admin", "password": "Admin@2026"
    })
    if code == 200:
        return body["data"]["access_token"], body["data"]["refresh_token"]
    else:
        print(f"LOGIN FAILED: {code} {body}")
        sys.exit(1)

# ============================================================
print("=" * 70)
print("  ERP Auth Module API Integration Test Report")
print("=" * 70)

# Login first
print("\n[Auth] Logging in as admin...")
token, refresh_token = login()
log("AUTH-01", "POST /auth/login", 200, 0, "Obtained access + refresh tokens")
print(f"  Token length: {len(token)}, Refresh length: {len(refresh_token)}")

# ============================================================
# PART 1: Auth Endpoints
# ============================================================
print("\n" + "-" * 70)
print("PART 1: Auth Endpoints (Public + Protected)")
print("-" * 70)

# 1.1 Login with wrong password
code, body, ms = api("POST", "/api/v1/auth/login", {"username": "admin", "password": "wrong"})
log("1.1", "Login - wrong password", code, ms, f"Expected 401, got {code}", expect_pass=False)

# 1.2 Login with missing fields
code, body, ms = api("POST", "/api/v1/auth/login", {"username": "admin"})
log("1.2", "Login - missing password field", code, ms, f"Expected 422, got {code}", expect_pass=False)

# 1.3 Login with empty body
code, body, ms = api("POST", "/api/v1/auth/login", {})
log("1.3", "Login - empty body", code, ms, f"Expected 422, got {code}", expect_pass=False)

# 1.4 SQL injection attempt
code, body, ms = api("POST", "/api/v1/auth/login", {"username": "admin' OR 1=1 --", "password": "x"})
log("1.4", "Login - SQL injection attempt", code, ms, f"Expected 401, got {code}", expect_pass=False)

# 1.5 Refresh token
code, body, ms = api("POST", "/api/v1/auth/refresh", {"refresh_token": refresh_token})
if code == 200:
    new_refresh = body["data"]["refresh_token"]
    log("1.5", "Refresh token - valid", code, ms, f"Got new tokens, expires_in={body['data'].get('expires_in')}")
else:
    new_refresh = refresh_token
    log("1.5", "Refresh token - valid", code, ms, f"FAIL: {body}", expect_pass=False)

# 1.6 Reuse old refresh token
code, body, ms = api("POST", "/api/v1/auth/refresh", {"refresh_token": refresh_token})
if code != 200:
    log("1.6", "Refresh - reuse old token", code, ms, f"Correctly rejected old token (HTTP {code})", expect_pass=False)
else:
    log("1.6", "Refresh - reuse old token", code, ms, f"SECURITY: Old refresh token still accepted!", expect_pass=False)

# 1.7 Invalid refresh token
code, body, ms = api("POST", "/api/v1/auth/refresh", {"refresh_token": "invalid.token"})
log("1.7", "Refresh - invalid token", code, ms, f"Expected 401, got {code}", expect_pass=False)

# 1.8 Logout without token
code, body, ms = api("POST", "/api/v1/auth/logout")
log("1.8", "Logout - no token", code, ms, f"Expected 401, got {code}", expect_pass=False)

# ============================================================
# PART 2: User Management
# ============================================================
print("\n" + "-" * 70)
print("PART 2: User Management (CRUD)")
print("-" * 70)

# 2.1 GET /users list
code, body, ms = api("GET", "/api/v1/users", tok=token)
data = body.get("data", {})
log("2.1", "GET /users - list all", code, ms,
    f"total={data.get('total')}, page={data.get('page')}, items={len(data.get('items', []))}")

# 2.2 GET /users with pagination
code, body, ms = api("GET", "/api/v1/users?page=1&page_size=1", tok=token)
data = body.get("data", {})
log("2.2", "GET /users?page=1&page_size=1", code, ms,
    f"total={data.get('total')}, page={data.get('page')}, page_size={data.get('page_size')}, items={len(data.get('items',[]))}")

# 2.3 Create user
code, body, ms = api("POST", "/api/v1/users", {
    "username": "test_user_api",
    "password": "Test@2026!",
    "display_name": "API Test User",
    "email": "test@api.com",
    "phone": "13800138000"
}, tok=token)
ud = body.get("data", {})
test_user_id = ud.get("id", "") if ud else ""
if test_user_id:
    created_resources["users"].append(test_user_id)
    pw_leaked = "password" in ud
    log("2.3", "POST /users - create user", code, ms,
        f"id={test_user_id[:20]}..., pw_in_resp={pw_leaked}, status={ud.get('status')}, version={ud.get('version')}")
else:
    log("2.3", "POST /users - create user", code, ms, f"FAIL: {json.dumps(body, ensure_ascii=False)[:200]}", expect_pass=False)

# 2.4 Duplicate username
code, body, ms = api("POST", "/api/v1/users", {
    "username": "test_user_api",
    "password": "Test@2026!",
    "display_name": "Dup"
}, tok=token)
log("2.4", "POST /users - duplicate username", code, ms, f"Expected 400/409, got {code}", expect_pass=False)

# 2.5 Missing required fields
code, body, ms = api("POST", "/api/v1/users", {"display_name": "No user"}, tok=token)
log("2.5", "POST /users - missing fields", code, ms, f"Expected 422, got {code}", expect_pass=False)

# 2.6 Short password
code, body, ms = api("POST", "/api/v1/users", {
    "username": "shortpwd", "password": "12", "display_name": "Short"
}, tok=token)
log("2.6", "POST /users - short password", code, ms, f"Expected validation error, got {code}", expect_pass=False)

if test_user_id:
    # 2.7 GET single user
    code, body, ms = api("GET", f"/api/v1/users/{test_user_id}", tok=token)
    ud = body.get("data", {})
    if ud:
        pw_leak = "password" in ud
        log("2.7", "GET /users/{id}", code, ms,
            f"found={ud.get('id')==test_user_id}, pw_leaked={pw_leak}, email={ud.get('email')}")
    else:
        log("2.7", "GET /users/{id}", code, ms, f"FAIL: {body}", expect_pass=False)

    # 2.8 GET nonexistent user
    code, body, ms = api("GET", "/api/v1/users/00000000-0000-0000-0000-000000000000", tok=token)
    log("2.8", "GET /users/nonexistent", code, ms, f"Expected 404, got {code}", expect_pass=False)

    # 2.9 PUT - update user
    code, body, ms = api("PUT", f"/api/v1/users/{test_user_id}", {
        "display_name": "Updated User",
        "email": "updated@api.com",
        "version": 1
    }, tok=token)
    ud = body.get("data", {})
    if ud:
        log("2.9", "PUT /users/{id} - update", code, ms,
            f"email={ud.get('email')}, version={ud.get('version')}")
    else:
        log("2.9", "PUT /users/{id} - update", code, ms, f"FAIL: {body}", expect_pass=False)

    # 2.10 Optimistic lock conflict
    code, body, ms = api("PUT", f"/api/v1/users/{test_user_id}", {
        "display_name": "Conflict", "version": 999
    }, tok=token)
    log("2.10", "PUT /users/{id} - version conflict", code, ms,
        f"Expected 409/400, got {code}", expect_pass=False)

    # 2.11 Assign roles
    code_r, body_r, _ = api("GET", "/api/v1/roles", tok=token)
    roles = body_r.get("data", {}).get("items", [])
    if roles:
        rid = roles[0]["id"]
        code, body, ms = api("POST", f"/api/v1/users/{test_user_id}/roles", {"role_ids": [rid]}, tok=token)
        log("2.11", "POST /users/{id}/roles - assign role", code, ms, f"Assigned role {rid[:20]}...")
    else:
        log("2.11", "POST /users/{id}/roles", 0, 0, "SKIP: no roles available")
else:
    print("  [SKIP] Cannot test user GET/PUT/DELETE - creation failed")

# ============================================================
# PART 3: Role Management
# ============================================================
print("\n" + "-" * 70)
print("PART 3: Role Management (CRUD)")
print("-" * 70)

# 3.1 GET /roles list
code, body, ms = api("GET", "/api/v1/roles", tok=token)
data = body.get("data", {})
roles = data.get("items", [])
log("3.1", "GET /roles - list all", code, ms,
    f"total={data.get('total')}, items={len(roles)}")
if roles:
    print(f"  Roles: {', '.join(r.get('code','?') for r in roles)}")

# 3.2 Create role
code, body, ms = api("POST", "/api/v1/roles", {
    "name": "Test Role API",
    "code": "test_role_api",
    "description": "Role created by API test"
}, tok=token)
rd = body.get("data", {})
test_role_id = rd.get("id", "") if rd else ""
if test_role_id:
    created_resources["roles"].append(test_role_id)
    log("3.2", "POST /roles - create role", code, ms,
        f"id={test_role_id[:20]}..., code={rd.get('code')}, is_system={rd.get('is_system')}")
else:
    log("3.2", "POST /roles - create role", code, ms, f"FAIL: {json.dumps(body, ensure_ascii=False)[:200]}", expect_pass=False)

# 3.3 Duplicate role code
code, body, ms = api("POST", "/api/v1/roles", {
    "name": "Dup Role", "code": "test_role_api"
}, tok=token)
log("3.3", "POST /roles - duplicate code", code, ms, f"Expected 400/409, got {code}", expect_pass=False)

# 3.4 Missing fields
code, body, ms = api("POST", "/api/v1/roles", {"name": "No code"}, tok=token)
log("3.4", "POST /roles - missing fields", code, ms, f"Expected 422, got {code}", expect_pass=False)

if test_role_id:
    # 3.5 GET single role
    code, body, ms = api("GET", f"/api/v1/roles/{test_role_id}", tok=token)
    rd = body.get("data", {})
    if rd:
        log("3.5", "GET /roles/{id}", code, ms,
            f"code={rd.get('code')}, name={rd.get('name')}, is_system={rd.get('is_system')}")
    else:
        log("3.5", "GET /roles/{id}", code, ms, f"FAIL: {body}", expect_pass=False)

    # 3.6 Update role
    code, body, ms = api("PUT", f"/api/v1/roles/{test_role_id}", {
        "name": "Updated Test Role",
        "description": "Updated description",
        "version": 1
    }, tok=token)
    rd = body.get("data", {})
    if rd:
        log("3.6", "PUT /roles/{id} - update", code, ms,
            f"name={rd.get('name')}, version={rd.get('version')}")
    else:
        log("3.6", "PUT /roles/{id} - update", code, ms, f"FAIL: {body}", expect_pass=False)

    # 3.7 GET role permissions
    code, body, ms = api("GET", f"/api/v1/roles/{test_role_id}/permissions", tok=token)
    perms = body.get("data", [])
    log("3.7", "GET /roles/{id}/permissions", code, ms,
        f"permissions count={len(perms) if isinstance(perms, list) else 'N/A'}")

    # 3.8 Assign permissions to role
    code_p, body_p, _ = api("GET", "/api/v1/permissions", tok=token)
    all_perms = body_p.get("data", {})
    perm_items = all_perms.get("items", []) if isinstance(all_perms, dict) else all_perms
    if perm_items and len(perm_items) > 0:
        perm_ids = [p["id"] for p in perm_items[:3]]
        code, body, ms = api("POST", f"/api/v1/roles/{test_role_id}/permissions", {
            "permission_ids": perm_ids
        }, tok=token)
        log("3.8", "POST /roles/{id}/permissions - assign", code, ms,
            f"Assigned {len(perm_ids)} permissions")
    else:
        log("3.8", "POST /roles/{id}/permissions", 0, 0, "SKIP: no permissions found")
else:
    print("  [SKIP] Cannot test role GET/PUT/DELETE - creation failed")

# ============================================================
# PART 4: Permissions
# ============================================================
print("\n" + "-" * 70)
print("PART 4: Permissions")
print("-" * 70)

code, body, ms = api("GET", "/api/v1/permissions", tok=token)
data = body.get("data", {})
if isinstance(data, dict):
    perms = data.get("items", [])
    total = data.get("total", len(perms))
else:
    perms = data if isinstance(data, list) else []
    total = len(perms)
log("4.1", "GET /permissions - list all", code, ms, f"total={total}, items={len(perms)}")
if perms:
    print(f"  Sample: {perms[0].get('code', 'N/A')} - {perms[0].get('name', 'N/A')}")

# ============================================================
# PART 5: Organization Management
# ============================================================
print("\n" + "-" * 70)
print("PART 5: Organization Management (CRUD)")
print("-" * 70)

# 5.1 GET /organizations list
code, body, ms = api("GET", "/api/v1/organizations", tok=token)
data = body.get("data", {})
orgs = data.get("items", []) if isinstance(data, dict) else (data if isinstance(data, list) else [])
log("5.1", "GET /organizations - list", code, ms,
    f"total={data.get('total') if isinstance(data, dict) else len(orgs)}, items={len(orgs)}")

# 5.2 Create organization
code, body, ms = api("POST", "/api/v1/organizations", {
    "name": "Test Org API",
    "code": "TEST_ORG_API",
    "description": "Organization created by API test"
}, tok=token)
od = body.get("data", {})
test_org_id = od.get("id", "") if od else ""
if test_org_id:
    created_resources["organizations"].append(test_org_id)
    log("5.2", "POST /organizations - create", code, ms,
        f"id={test_org_id[:20]}..., code={od.get('code')}")
else:
    log("5.2", "POST /organizations - create", code, ms, f"FAIL: {json.dumps(body, ensure_ascii=False)[:200]}", expect_pass=False)

# 5.3 Duplicate org code
code, body, ms = api("POST", "/api/v1/organizations", {
    "name": "Dup Org", "code": "TEST_ORG_API"
}, tok=token)
log("5.3", "POST /organizations - duplicate code", code, ms, f"Expected error, got {code}", expect_pass=False)

if test_org_id:
    # 5.4 Update organization
    code, body, ms = api("PUT", f"/api/v1/organizations/{test_org_id}", {
        "name": "Updated Org",
        "description": "Updated",
        "version": 1
    }, tok=token)
    od = body.get("data", {})
    if od:
        log("5.4", "PUT /organizations/{id} - update", code, ms,
            f"name={od.get('name')}, version={od.get('version')}")
    else:
        log("5.4", "PUT /organizations/{id} - update", code, ms, f"FAIL: {json.dumps(body, ensure_ascii=False)[:200]}", expect_pass=False)

# ============================================================
# PART 6: Department Management
# ============================================================
print("\n" + "-" * 70)
print("PART 6: Department Management (CRUD)")
print("-" * 70)

if test_org_id:
    # 6.1 GET departments under org
    code, body, ms = api("GET", f"/api/v1/organizations/{test_org_id}/departments", tok=token)
    data = body.get("data", {})
    depts = data.get("items", []) if isinstance(data, dict) else (data if isinstance(data, list) else [])
    log("6.1", "GET /organizations/{id}/departments", code, ms,
        f"items={len(depts) if isinstance(depts, list) else 'N/A'}")

    # 6.2 Create department
    code, body, ms = api("POST", f"/api/v1/organizations/{test_org_id}/departments", {
        "name": "Test Dept API",
        "code": "TEST_DEPT_API",
        "description": "Department by API test"
    }, tok=token)
    dd = body.get("data", {})
    test_dept_id = dd.get("id", "") if dd else ""
    if test_dept_id:
        created_resources["departments"].append(test_dept_id)
        log("6.2", "POST /organizations/{id}/departments - create", code, ms,
            f"id={test_dept_id[:20]}..., code={dd.get('code')}")
    else:
        log("6.2", "POST .../departments - create", code, ms, f"FAIL: {json.dumps(body, ensure_ascii=False)[:200]}", expect_pass=False)

    if test_dept_id:
        # 6.3 Update department
        code, body, ms = api("PUT", f"/api/v1/departments/{test_dept_id}", {
            "name": "Updated Dept",
            "description": "Updated",
            "version": 1
        }, tok=token)
        dd = body.get("data", {})
        if dd:
            log("6.3", "PUT /departments/{id} - update", code, ms,
                f"name={dd.get('name')}, version={dd.get('version')}")
        else:
            log("6.3", "PUT /departments/{id} - update", code, ms, f"FAIL: {json.dumps(body, ensure_ascii=False)[:200]}", expect_pass=False)

        # 6.4 GET departments with invalid org
        code, body, ms = api("GET", "/api/v1/organizations/00000000-0000-0000-0000-000000000000/departments", tok=token)
        data = body.get("data", {})
        depts_inv = data.get("items", []) if isinstance(data, dict) else []
        log("6.4", "GET /organizations/invalid/departments", code, ms,
            f"items={len(depts_inv) if isinstance(depts_inv, list) else 'N/A'}, expected empty")
else:
    print("  [SKIP] Cannot test departments - no org created")
    test_dept_id = ""

# ============================================================
# PART 7: Position Management
# ============================================================
print("\n" + "-" * 70)
print("PART 7: Position Management (CRUD)")
print("-" * 70)

if test_dept_id:
    # 7.1 GET positions under dept
    code, body, ms = api("GET", f"/api/v1/departments/{test_dept_id}/positions", tok=token)
    data = body.get("data", {})
    positions = data.get("items", []) if isinstance(data, dict) else (data if isinstance(data, list) else [])
    log("7.1", "GET /departments/{id}/positions", code, ms,
        f"items={len(positions) if isinstance(positions, list) else 'N/A'}")

    # 7.2 Create position
    code, body, ms = api("POST", f"/api/v1/departments/{test_dept_id}/positions", {
        "name": "Test Position API",
        "code": "TEST_POS_API",
        "description": "Position by API test"
    }, tok=token)
    pd = body.get("data", {})
    test_pos_id = pd.get("id", "") if pd else ""
    if test_pos_id:
        created_resources["positions"].append(test_pos_id)
        log("7.2", "POST .../positions - create", code, ms,
            f"id={test_pos_id[:20]}..., code={pd.get('code')}")
    else:
        log("7.2", "POST .../positions - create", code, ms, f"FAIL: {json.dumps(body, ensure_ascii=False)[:200]}", expect_pass=False)

    if test_pos_id:
        # 7.3 Update position
        code, body, ms = api("PUT", f"/api/v1/positions/{test_pos_id}", {
            "name": "Updated Position",
            "description": "Updated",
            "version": 1
        }, tok=token)
        pd = body.get("data", {})
        if pd:
            log("7.3", "PUT /positions/{id} - update", code, ms,
                f"name={pd.get('name')}, version={pd.get('version')}")
        else:
            log("7.3", "PUT /positions/{id} - update", code, ms, f"FAIL: {json.dumps(body, ensure_ascii=False)[:200]}", expect_pass=False)

        # 7.4 Duplicate position code
        code, body, ms = api("POST", f"/api/v1/departments/{test_dept_id}/positions", {
            "name": "Dup Pos", "code": "TEST_POS_API"
        }, tok=token)
        log("7.4", "POST .../positions - duplicate code", code, ms, f"Expected error, got {code}", expect_pass=False)
else:
    print("  [SKIP] Cannot test positions - no department created")

# ============================================================
# PART 8: Security & Permission Validation
# ============================================================
print("\n" + "-" * 70)
print("PART 8: Security & Permission Validation")
print("-" * 70)

# 8.1 No token
code, body, ms = api("GET", "/api/v1/users")
log("8.1", "GET /users - no token", code, ms, f"Expected 401, got {code}", expect_pass=False)

# 8.2 Invalid token
code, body, ms = api("GET", "/api/v1/users", tok="invalid.jwt.token")
log("8.2", "GET /users - invalid token", code, ms, f"Expected 401, got {code}", expect_pass=False)

# 8.3 Tampered token
tampered = token[:-5] + "XXXXX"
code, body, ms = api("GET", "/api/v1/users", tok=tampered)
log("8.3", "GET /users - tampered token", code, ms, f"Expected 401, got {code}", expect_pass=False)

# 8.4 Access other tenant data (tenant_id in URL)
code, body, ms = api("GET", "/api/v1/users?tenant_id=00000000-0000-0000-0000-000000000000", tok=token)
log("8.4", "GET /users - cross-tenant attempt", code, ms, f"Should ignore tenant_id param, got {code}")

# 8.5 POST /users - XSS attempt
code, body, ms = api("POST", "/api/v1/users", {
    "username": "xss_user",
    "password": "Test@2026!",
    "display_name": "<script>alert('xss')</script>"
}, tok=token)
if code == 201:
    xss_user_id = body.get("data", {}).get("id", "")
    if xss_user_id:
        created_resources["users"].append(xss_user_id)
    log("8.5", "POST /users - XSS payload", code, ms,
        f"Created with XSS payload - check if sanitized (id={xss_user_id[:20] if xss_user_id else 'N/A'})")
else:
    log("8.5", "POST /users - XSS payload", code, ms, f"Blocked or error: {code}")

# ============================================================
# PART 9: Cleanup - Delete Created Resources
# ============================================================
print("\n" + "-" * 70)
print("PART 9: Cleanup - Deleting Created Resources (Soft Delete)")
print("-" * 70)

# Delete positions
for pid in created_resources["positions"]:
    code, body, ms = api("DELETE", f"/api/v1/positions/{pid}", tok=token)
    log("9.1", f"DELETE /positions/{pid[:20]}...", code, ms, f"code={code}")

# Delete departments
for did in created_resources["departments"]:
    code, body, ms = api("DELETE", f"/api/v1/departments/{did}", tok=token)
    log("9.2", f"DELETE /departments/{did[:20]}...", code, ms, f"code={code}")

# Delete test user(s)
for uid in created_resources["users"]:
    code, body, ms = api("DELETE", f"/api/v1/users/{uid}", tok=token)
    log("9.3", f"DELETE /users/{uid[:20]}...", code, ms, f"code={code}")

# Delete role (skip admin/viewer system roles)
for rid in created_resources["roles"]:
    code, body, ms = api("DELETE", f"/api/v1/roles/{rid}", tok=token)
    log("9.4", f"DELETE /roles/{rid[:20]}...", code, ms, f"code={code}")

# Delete organizations
for oid in created_resources["organizations"]:
    code, body, ms = api("DELETE", f"/api/v1/organizations/{oid}", tok=token)
    log("9.5", f"DELETE /organizations/{oid[:20]}...", code, ms, f"code={code}")

# ============================================================
# FINAL SUMMARY
# ============================================================
print("\n" + "=" * 70)
print("  TEST SUMMARY")
print("=" * 70)

total = len(results)
passed = sum(1 for r in results if r["status"] == "PASS")
failed = sum(1 for r in results if r["status"] == "FAIL")

print(f"\n  Total Tests: {total}")
print(f"  Passed:      {passed}")
print(f"  Failed:      {failed}")
print(f"  Pass Rate:   {passed/total*100:.1f}%")

# Performance stats
all_ms = [r["ms"] for r in results if r["ms"] > 0]
if all_ms:
    avg_ms = sum(all_ms) / len(all_ms)
    max_ms = max(all_ms)
    min_ms = min(all_ms)
    print(f"\n  Avg Response: {avg_ms:.0f}ms")
    print(f"  Max Response: {max_ms}ms")
    print(f"  Min Response: {min_ms}ms")

# Failed tests detail
if failed > 0:
    print(f"\n  FAILED TESTS:")
    for r in results:
        if r["status"] == "FAIL":
            print(f"    [{r['id']}] {r['name']}: HTTP {r['code']} | {r['detail'][:100]}")

print("\n" + "=" * 70)
