/**
 * @license
 * Copyright 2025 Vybestack LLC
 * SPDX-License-Identifier: Apache-2.0
 */
/**
 * Platform-specific test matrix for credential proxy Unix socket behavior.
 *
 * Tests platform-conditional socket creation, permissions, peer credential
 * verification, and socket path handling across Linux and macOS.
 *
 * @plan:PLAN-20250214-CREDPROXY.P38
 * @requirement R4.1, R4.2, R4.3, R27.1, R27.2, R27.3
 */
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import * as net from 'node:net';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { ProxySocketClient } from '@vybestack/llxprt-code-core';
import { CredentialProxyServer } from '../credential-proxy-server.js';
// ─── Test Helpers ────────────────────────────────────────────────────────────
const isLinux = process.platform === 'linux';
const isMacOS = process.platform === 'darwin';
const isWindows = process.platform === 'win32';
class InMemoryTokenStore {
    tokens = new Map();
    key(provider, bucket) {
        return `${provider}::${bucket ?? 'default'}`;
    }
    async saveToken(provider, token, bucket) {
        this.tokens.set(this.key(provider, bucket), { ...token });
    }
    async getToken(provider, bucket) {
        return this.tokens.get(this.key(provider, bucket)) ?? null;
    }
    async removeToken(provider, bucket) {
        this.tokens.delete(this.key(provider, bucket));
    }
    async listProviders() {
        const providers = new Set();
        for (const composite of this.tokens.keys()) {
            const [provider] = composite.split('::');
            if (provider)
                providers.add(provider);
        }
        return Array.from(providers);
    }
    async listBuckets(provider) {
        const buckets = [];
        for (const composite of this.tokens.keys()) {
            const [tokenProvider, bucket] = composite.split('::');
            if (tokenProvider === provider && bucket)
                buckets.push(bucket);
        }
        return buckets;
    }
    async getBucketStats() {
        return null;
    }
    async acquireRefreshLock() {
        return true;
    }
    async releaseRefreshLock() {
        // no-op
    }
    async acquireAuthLock() {
        return true;
    }
    async releaseAuthLock() {
        // no-op
    }
}
class InMemoryProviderKeyStorage {
    keys = new Map();
    async saveKey(name, apiKey) {
        this.keys.set(name, apiKey);
    }
    async getKey(name) {
        return this.keys.get(name) ?? null;
    }
    async deleteKey(name) {
        return this.keys.delete(name);
    }
    async listKeys() {
        return Array.from(this.keys.keys());
    }
    async hasKey(name) {
        return this.keys.has(name);
    }
}
// ─── Platform Matrix Tests ───────────────────────────────────────────────────
describe('Platform Matrix Tests (Phase 38)', () => {
    let tmpDir;
    let server = null;
    beforeEach(() => {
        tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pm-'));
    });
    afterEach(async () => {
        if (server) {
            try {
                await server.stop();
            }
            catch {
                // ignore cleanup errors
            }
            server = null;
        }
        fs.rmSync(tmpDir, { recursive: true, force: true });
    });
    // ─────────────────────────────────────────────────────────────────────────
    // Unix Socket Creation
    // ─────────────────────────────────────────────────────────────────────────
    describe('Unix socket creation', () => {
        /**
         * @requirement R3.1
         * @scenario Socket created with correct permissions
         * @given A CredentialProxyServer is started
         * @when The socket file is created
         * @then Socket file has mode 0o600 (owner read/write only)
         */
        it.skipIf(isWindows)('creates socket with correct permissions (0o600)', async () => {
            const tokenStore = new InMemoryTokenStore();
            const keyStorage = new InMemoryProviderKeyStorage();
            server = new CredentialProxyServer({
                tokenStore,
                providerKeyStorage: keyStorage,
                socketDir: tmpDir,
            });
            const socketPath = await server.start();
            const stat = fs.statSync(socketPath);
            expect(stat.isSocket()).toBe(true);
            expect(socketPath.endsWith('.sock')).toBe(true);
            // Verify socket file permissions (owner read/write only)
            const mode = stat.mode & 0o777;
            expect(mode).toBe(0o600);
        });
    });
    // ─────────────────────────────────────────────────────────────────────────
    // Subdirectory Permissions
    // ─────────────────────────────────────────────────────────────────────────
    describe('Subdirectory permissions', () => {
        /**
         * @requirement R3.2
         * @scenario Per-user directory created with correct permissions
         * @given A CredentialProxyServer is started
         * @when The socket directory is created
         * @then Directory has mode 0o700 (owner only)
         */
        it.skipIf(isWindows)('creates per-user directory with correct permissions (0o700)', async () => {
            const tokenStore = new InMemoryTokenStore();
            const keyStorage = new InMemoryProviderKeyStorage();
            server = new CredentialProxyServer({
                tokenStore,
                providerKeyStorage: keyStorage,
                socketDir: tmpDir,
            });
            const socketPath = await server.start();
            const dir = path.dirname(socketPath);
            const stat = fs.statSync(dir);
            expect(stat.isDirectory()).toBe(true);
            // Check that directory has restricted permissions (owner only)
            const mode = stat.mode & 0o777;
            expect(mode).toBe(0o700);
        });
    });
    // ─────────────────────────────────────────────────────────────────────────
    // Realpath Resolution (macOS)
    // ─────────────────────────────────────────────────────────────────────────
    describe('Realpath resolution', () => {
        /**
         * @requirement R3.4
         * @scenario macOS /var → /private/var resolved correctly
         * @given Running on macOS where /var is symlink to /private/var
         * @when tmpdir is used for socket path
         * @then The resolved realpath is used
         */
        it.skipIf(!isMacOS)('macOS: resolves /var → /private/var symlink correctly', () => {
            const rawTmpdir = os.tmpdir();
            const resolvedTmpdir = fs.realpathSync(rawTmpdir);
            // On macOS, /var is typically a symlink to /private/var
            if (rawTmpdir.startsWith('/var')) {
                expect(resolvedTmpdir.startsWith('/private/var')).toBe(true);
            }
            else {
                // If not starting with /var, they should still match
                expect(fs.existsSync(resolvedTmpdir)).toBe(true);
            }
        });
        /**
         * @requirement R3.4
         * @scenario Socket path uses resolved realpath
         * @given A CredentialProxyServer on any platform
         * @when Socket path is generated
         * @then Path uses fs.realpathSync(os.tmpdir())
         */
        it.skipIf(isWindows)('socket path uses resolved tmpdir (fs.realpathSync)', async () => {
            const tokenStore = new InMemoryTokenStore();
            const keyStorage = new InMemoryProviderKeyStorage();
            // Don't pass a custom socketDir - let the server use its default path generation
            // which resolves os.tmpdir() to canonical path (handles macOS /var → /private/var)
            server = new CredentialProxyServer({
                tokenStore,
                providerKeyStorage: keyStorage,
                // No socketDir - server will use fs.realpathSync(os.tmpdir())
            });
            const socketPath = await server.start();
            const resolvedSystemTmpDir = fs.realpathSync(os.tmpdir());
            // Socket path should start with the resolved system tmpdir
            expect(socketPath.startsWith(resolvedSystemTmpDir)).toBe(true);
        });
    });
    // ─────────────────────────────────────────────────────────────────────────
    // Peer Credential Verification — Linux
    // ─────────────────────────────────────────────────────────────────────────
    describe('Peer credential verification — Linux', () => {
        /**
         * @requirement R4.1
         * @scenario SO_PEERCRED returns correct UID on Linux
         * @given Running on Linux where SO_PEERCRED is available
         * @when A client connects to the server
         * @then The peer UID can be verified (best-effort test)
         *
         * NOTE: SO_PEERCRED is a kernel-level feature. We verify the socket
         * connection works; actual SO_PEERCRED verification happens in the
         * server implementation (not directly testable without native bindings).
         */
        it.skipIf(!isLinux)('Linux: socket connection works with peer credentials available', async () => {
            const tokenStore = new InMemoryTokenStore();
            const keyStorage = new InMemoryProviderKeyStorage();
            server = new CredentialProxyServer({
                tokenStore,
                providerKeyStorage: keyStorage,
                socketDir: tmpDir,
            });
            const socketPath = await server.start();
            // Create a raw socket connection to verify connectivity
            const socket = await new Promise((resolve, reject) => {
                const s = net.createConnection(socketPath, () => resolve(s));
                s.once('error', reject);
            });
            expect(socket).toBeInstanceOf(net.Socket);
            socket.destroy();
        });
    });
    // ─────────────────────────────────────────────────────────────────────────
    // Peer Credential Verification — macOS
    // ─────────────────────────────────────────────────────────────────────────
    describe('Peer credential verification — macOS', () => {
        /**
         * @requirement R4.2
         * @scenario LOCAL_PEERPID returns PID on macOS (best-effort)
         * @given Running on macOS where LOCAL_PEERPID may be available
         * @when A client connects to the server
         * @then Connection succeeds (PID verification is best-effort logging)
         */
        it.skipIf(!isMacOS)('macOS: socket connection works (LOCAL_PEERPID is best-effort)', async () => {
            const tokenStore = new InMemoryTokenStore();
            const keyStorage = new InMemoryProviderKeyStorage();
            server = new CredentialProxyServer({
                tokenStore,
                providerKeyStorage: keyStorage,
                socketDir: tmpDir,
            });
            const socketPath = await server.start();
            // Verify connection works on macOS
            const socket = await new Promise((resolve, reject) => {
                const s = net.createConnection(socketPath, () => resolve(s));
                s.once('error', reject);
            });
            expect(socket).toBeInstanceOf(net.Socket);
            socket.destroy();
        });
    });
    // ─────────────────────────────────────────────────────────────────────────
    // Peer Credential Verification — Fallback
    // ─────────────────────────────────────────────────────────────────────────
    describe('Peer credential verification — fallback', () => {
        /**
         * @requirement R4.3
         * @scenario Fallback when neither SO_PEERCRED nor LOCAL_PEERPID available
         * @given Running on a platform without peer credential support
         * @when A client connects to the server
         * @then Connection succeeds (socket permissions + nonce are primary defense)
         */
        it.skipIf(isWindows)('connection succeeds even without peer credential support', async () => {
            const tokenStore = new InMemoryTokenStore();
            const keyStorage = new InMemoryProviderKeyStorage();
            server = new CredentialProxyServer({
                tokenStore,
                providerKeyStorage: keyStorage,
                socketDir: tmpDir,
            });
            const socketPath = await server.start();
            // Use ProxySocketClient to verify full handshake works
            const client = new ProxySocketClient(socketPath);
            await client.ensureConnected();
            const response = await client.request('list_providers', {});
            expect(response.ok).toBe(true);
            client.close();
        });
    });
    // ─────────────────────────────────────────────────────────────────────────
    // Socket Path Length
    // ─────────────────────────────────────────────────────────────────────────
    describe('Socket path length', () => {
        /**
         * @requirement R3.1
         * @scenario Socket path fits within platform limits
         * @given The maximum socket path generated by the server
         * @when Path length is measured
         * @then Path length is less than ~104 chars (macOS limit)
         */
        it.skipIf(isWindows)('socket path fits within platform socket path limits', async () => {
            const tokenStore = new InMemoryTokenStore();
            const keyStorage = new InMemoryProviderKeyStorage();
            server = new CredentialProxyServer({
                tokenStore,
                providerKeyStorage: keyStorage,
                socketDir: tmpDir,
            });
            const socketPath = await server.start();
            // macOS has ~104 char limit, Linux typically ~108
            // Our generated path should be well under this
            expect(socketPath.length).toBeLessThan(104);
        });
        /**
         * @requirement R3.1
         * @scenario Worst-case socket path length verification
         * @given Maximum expected tmpdir + uid + pid + nonce
         * @when Path length is calculated
         * @then Stays within limits
         */
        it('calculates worst-case socket path length', () => {
            // Simulate worst-case path calculation
            const resolvedTmpdir = fs.realpathSync(os.tmpdir());
            const uid = process.getuid?.() ?? 99999;
            const maxPid = 99999;
            // 128-bit nonce in base64url = 22 chars
            const nonce = 'AAAAAAAAAAAAAAAAAAAAAA';
            // Use short directory name "lc-" to fit within macOS socket path limits
            const worstCasePath = path.join(resolvedTmpdir, `lc-${uid}`, `${maxPid}-${nonce}.sock`);
            // Should be at most 104 chars for macOS compatibility
            expect(worstCasePath.length).toBeLessThanOrEqual(104);
        });
    });
    // ─────────────────────────────────────────────────────────────────────────
    // Stale Socket Cleanup
    // ─────────────────────────────────────────────────────────────────────────
    describe('Stale socket cleanup', () => {
        /**
         * @requirement R25.1
         * @scenario Server can start in directory with existing files
         * @given A directory contains existing .sock files (simulating stale sockets)
         * @when Server attempts to start
         * @then Server generates unique path and starts successfully
         */
        it.skipIf(isWindows)('starts successfully in directory with existing socket files', async () => {
            const tokenStore = new InMemoryTokenStore();
            const keyStorage = new InMemoryProviderKeyStorage();
            // Create a file to simulate a stale socket (regular file, not actual socket)
            // This tests that the server can start in a directory with existing files
            const staleFilePath = path.join(tmpDir, `${process.pid}-stale1234567890123456.sock`);
            fs.writeFileSync(staleFilePath, 'stale socket placeholder');
            // Verify the file exists
            expect(fs.existsSync(staleFilePath)).toBe(true);
            // Create a CredentialProxyServer - it generates unique paths with random nonces
            // so it won't conflict with existing files
            server = new CredentialProxyServer({
                tokenStore,
                providerKeyStorage: keyStorage,
                socketDir: tmpDir,
            });
            // Should start successfully
            const socketPath = await server.start();
            expect(fs.existsSync(socketPath)).toBe(true);
            expect(socketPath).not.toBe(staleFilePath); // Different path due to random nonce
            // Original file should still exist (server doesn't clean up other files)
            expect(fs.existsSync(staleFilePath)).toBe(true);
        });
    });
    // ─────────────────────────────────────────────────────────────────────────
    // Concurrent Socket Operations
    // ─────────────────────────────────────────────────────────────────────────
    describe('Concurrent socket operations', () => {
        /**
         * @requirement R25.5
         * @scenario Multiple requests over single socket handled correctly
         * @given A running credential proxy server
         * @when Multiple concurrent requests are sent
         * @then All requests receive correct responses
         */
        it.skipIf(isWindows)('handles multiple concurrent requests over single socket', async () => {
            const tokenStore = new InMemoryTokenStore();
            const keyStorage = new InMemoryProviderKeyStorage();
            // Pre-populate some tokens
            await tokenStore.saveToken('anthropic', {
                access_token: 'test-anthropic',
                expiry: Math.floor(Date.now() / 1000) + 3600,
                token_type: 'Bearer',
            }, 'default');
            await tokenStore.saveToken('openai', {
                access_token: 'test-openai',
                expiry: Math.floor(Date.now() / 1000) + 3600,
                token_type: 'Bearer',
            }, 'default');
            server = new CredentialProxyServer({
                tokenStore,
                providerKeyStorage: keyStorage,
                socketDir: tmpDir,
            });
            const socketPath = await server.start();
            const client = new ProxySocketClient(socketPath);
            // Send multiple concurrent requests
            const requests = [
                client.request('list_providers', {}),
                client.request('get_token', { provider: 'anthropic' }),
                client.request('get_token', { provider: 'openai' }),
                client.request('list_providers', {}),
            ];
            const results = await Promise.all(requests);
            // Verify all requests succeeded
            expect(results[0].ok).toBe(true);
            expect(results[1].ok).toBe(true);
            expect(results[1].data?.access_token).toBe('test-anthropic');
            expect(results[2].ok).toBe(true);
            expect(results[2].data?.access_token).toBe('test-openai');
            expect(results[3].ok).toBe(true);
            client.close();
        });
        /**
         * @requirement R25.5
         * @scenario Multiple clients connect to same socket
         * @given A running credential proxy server
         * @when Multiple clients connect simultaneously
         * @then All clients receive correct responses
         */
        it.skipIf(isWindows)('handles multiple simultaneous client connections', async () => {
            const tokenStore = new InMemoryTokenStore();
            const keyStorage = new InMemoryProviderKeyStorage();
            await tokenStore.saveToken('gemini', {
                access_token: 'test-gemini',
                expiry: Math.floor(Date.now() / 1000) + 3600,
                token_type: 'Bearer',
            }, 'default');
            server = new CredentialProxyServer({
                tokenStore,
                providerKeyStorage: keyStorage,
                socketDir: tmpDir,
            });
            const socketPath = await server.start();
            // Create multiple clients
            const clients = [
                new ProxySocketClient(socketPath),
                new ProxySocketClient(socketPath),
                new ProxySocketClient(socketPath),
            ];
            // Each client makes a request
            const results = await Promise.all(clients.map((client) => client.request('get_token', { provider: 'gemini' })));
            // All should succeed
            for (const result of results) {
                expect(result.ok).toBe(true);
                expect(result.data?.access_token).toBe('test-gemini');
            }
            // Clean up
            for (const client of clients) {
                client.close();
            }
        });
    });
});
//# sourceMappingURL=platform-matrix.test.js.map