import { assertEquals } from "../../../test/assert";

// ── Pure function copies (inlined to avoid pulling in server dependencies) ──

function sanitize(input: string | null | undefined): string | undefined {
    if (!input) return undefined;
    if (input.length > 50) {
        input = input.substring(0, 50);
    }
    return input
        .replace(/[^a-zA-Z0-9-]/g, "-")
        .replace(/-+/g, "-")
        .replace(/^-|-$/g, "");
}

function encodePath(path: string | null | undefined): string {
    if (!path) return "";
    return path.replace(/[^a-zA-Z0-9]/g, (ch) => {
        return ch.charCodeAt(0).toString(16);
    });
}

// ── Helpers ──────────────────────────────────────────────────────────

/**
 * Exact replica of the OLD key computation from upstream main.
 * Uses sanitize() for paths - this is what had the collision bug.
 */
function oldKeyComputation(
    resourceId: number,
    path: string | null,
    pathMatchType: string | null,
    rewritePath: string | null,
    rewritePathType: string | null
): string {
    const targetPath = sanitize(path) || "";
    const pmt = pathMatchType || "";
    const rp = rewritePath || "";
    const rpt = rewritePathType || "";
    const pathKey = [targetPath, pmt, rp, rpt].filter(Boolean).join("-");
    const mapKey = [resourceId, pathKey].filter(Boolean).join("-");
    return sanitize(mapKey) || "";
}

/**
 * Replica of the NEW key computation from our fix.
 * Uses encodePath() for paths - collision-free.
 */
function newKeyComputation(
    resourceId: number,
    path: string | null,
    pathMatchType: string | null,
    rewritePath: string | null,
    rewritePathType: string | null
): string {
    const targetPath = encodePath(path);
    const pmt = pathMatchType || "";
    const rp = rewritePath || "";
    const rpt = rewritePathType || "";
    const pathKey = [targetPath, pmt, rp, rpt].filter(Boolean).join("-");
    const mapKey = [resourceId, pathKey].filter(Boolean).join("-");
    return sanitize(mapKey) || "";
}

// ── Tests ────────────────────────────────────────────────────────────

function runTests() {
    console.log("Running path encoding tests...\n");

    let passed = 0;

    // ── encodePath unit tests ────────────────────────────────────────

    // Test 1: null/undefined/empty
    {
        assertEquals(encodePath(null), "", "null should return empty");
        assertEquals(
            encodePath(undefined),
            "",
            "undefined should return empty"
        );
        assertEquals(encodePath(""), "", "empty string should return empty");
        console.log("  PASS: encodePath handles null/undefined/empty");
        passed++;
    }

    // Test 2: root path
    {
        assertEquals(encodePath("/"), "2f", "/ should encode to 2f");
        console.log("  PASS: encodePath encodes root path");
        passed++;
    }

    // Test 3: alphanumeric passthrough
    {
        assertEquals(encodePath("/api"), "2fapi", "/api encodes slash only");
        assertEquals(encodePath("/v1"), "2fv1", "/v1 encodes slash only");
        assertEquals(encodePath("abc"), "abc", "plain alpha passes through");
        console.log("  PASS: encodePath preserves alphanumeric chars");
        passed++;
    }

    // Test 4: all special chars produce unique hex
    {
        const paths = ["/a/b", "/a-b", "/a.b", "/a_b", "/a b"];
        const results = paths.map((p) => encodePath(p));
        const unique = new Set(results);
        assertEquals(
            unique.size,
            paths.length,
            "all special-char paths must produce unique encodings"
        );
        console.log(
            "  PASS: encodePath produces unique output for different special chars"
        );
        passed++;
    }

    // Test 5: output is always alphanumeric (safe for Traefik names)
    {
        const paths = [
            "/",
            "/api",
            "/a/b",
            "/a-b",
            "/a.b",
            "/complex/path/here"
        ];
        for (const p of paths) {
            const e = encodePath(p);
            assertEquals(
                /^[a-zA-Z0-9]+$/.test(e),
                true,
                `encodePath("${p}") = "${e}" must be alphanumeric`
            );
        }
        console.log("  PASS: encodePath output is always alphanumeric");
        passed++;
    }

    // Test 6: deterministic
    {
        assertEquals(
            encodePath("/api"),
            encodePath("/api"),
            "same input same output"
        );
        assertEquals(
            encodePath("/a/b/c"),
            encodePath("/a/b/c"),
            "same input same output"
        );
        console.log("  PASS: encodePath is deterministic");
        passed++;
    }

    // Test 7: many distinct paths never collide
    {
        const paths = [
            "/",
            "/api",
            "/api/v1",
            "/api/v2",
            "/a/b",
            "/a-b",
            "/a.b",
            "/a_b",
            "/health",
            "/health/check",
            "/admin",
            "/admin/users",
            "/api/v1/users",
            "/api/v1/posts",
            "/app",
            "/app/dashboard"
        ];
        const encoded = new Set(paths.map((p) => encodePath(p)));
        assertEquals(
            encoded.size,
            paths.length,
            `expected ${paths.length} unique encodings, got ${encoded.size}`
        );
        console.log("  PASS: 16 realistic paths all produce unique encodings");
        passed++;
    }

    // ── Collision fix: the actual bug we're fixing ───────────────────

    // Test 8: /a/b and /a-b now have different keys (THE BUG FIX)
    {
        const keyAB = newKeyComputation(1, "/a/b", "prefix", null, null);
        const keyDash = newKeyComputation(1, "/a-b", "prefix", null, null);
        assertEquals(
            keyAB !== keyDash,
            true,
            "/a/b and /a-b MUST have different keys"
        );
        console.log("  PASS: collision fix - /a/b vs /a-b have different keys");
        passed++;
    }

    // Test 9: demonstrate the old bug - old code maps /a/b and /a-b to same key
    {
        const oldKeyAB = oldKeyComputation(1, "/a/b", "prefix", null, null);
        const oldKeyDash = oldKeyComputation(1, "/a-b", "prefix", null, null);
        assertEquals(
            oldKeyAB,
            oldKeyDash,
            "old code MUST have this collision (confirms the bug exists)"
        );
        console.log("  PASS: confirmed old code bug - /a/b and /a-b collided");
        passed++;
    }

    // Test 10: /api/v1 and /api-v1 - old code collision, new code fixes it
    {
        const oldKey1 = oldKeyComputation(1, "/api/v1", "prefix", null, null);
        const oldKey2 = oldKeyComputation(1, "/api-v1", "prefix", null, null);
        assertEquals(
            oldKey1,
            oldKey2,
            "old code collision for /api/v1 vs /api-v1"
        );

        const newKey1 = newKeyComputation(1, "/api/v1", "prefix", null, null);
        const newKey2 = newKeyComputation(1, "/api-v1", "prefix", null, null);
        assertEquals(
            newKey1 !== newKey2,
            true,
            "new code must separate /api/v1 and /api-v1"
        );
        console.log("  PASS: collision fix - /api/v1 vs /api-v1");
        passed++;
    }

    // Test 11: /app.v2 and /app/v2 and /app-v2 - three-way collision fixed
    {
        const a = newKeyComputation(1, "/app.v2", "prefix", null, null);
        const b = newKeyComputation(1, "/app/v2", "prefix", null, null);
        const c = newKeyComputation(1, "/app-v2", "prefix", null, null);
        const keys = new Set([a, b, c]);
        assertEquals(
            keys.size,
            3,
            "three paths must produce three unique keys"
        );
        console.log(
            "  PASS: collision fix - three-way /app.v2, /app/v2, /app-v2"
        );
        passed++;
    }

    // ── Edge cases ───────────────────────────────────────────────────

    // Test 12: same path in different resources - always separate
    {
        const key1 = newKeyComputation(1, "/api", "prefix", null, null);
        const key2 = newKeyComputation(2, "/api", "prefix", null, null);
        assertEquals(
            key1 !== key2,
            true,
            "different resources with same path must have different keys"
        );
        console.log("  PASS: edge case - same path, different resources");
        passed++;
    }

    // Test 13: same resource, different pathMatchType - separate keys
    {
        const exact = newKeyComputation(1, "/api", "exact", null, null);
        const prefix = newKeyComputation(1, "/api", "prefix", null, null);
        assertEquals(
            exact !== prefix,
            true,
            "exact vs prefix must have different keys"
        );
        console.log("  PASS: edge case - same path, different match types");
        passed++;
    }

    // Test 14: same resource and path, different rewrite config - separate keys
    {
        const noRewrite = newKeyComputation(1, "/api", "prefix", null, null);
        const withRewrite = newKeyComputation(
            1,
            "/api",
            "prefix",
            "/backend",
            "prefix"
        );
        assertEquals(
            noRewrite !== withRewrite,
            true,
            "with vs without rewrite must have different keys"
        );
        console.log("  PASS: edge case - same path, different rewrite config");
        passed++;
    }

    // Test 15: paths with special URL characters
    {
        const paths = ["/api?foo", "/api#bar", "/api%20baz", "/api+qux"];
        const keys = new Set(
            paths.map((p) => newKeyComputation(1, p, "prefix", null, null))
        );
        assertEquals(
            keys.size,
            paths.length,
            "special URL chars must produce unique keys"
        );
        console.log("  PASS: edge case - special URL characters in paths");
        passed++;
    }

    console.log(`\nAll ${passed} tests passed!`);
}

try {
    runTests();
} catch (error) {
    console.error("Test failed:", error);
    process.exit(1);
}
