/**
 * Regression guard for F8: MeshService.listStacksOnNode dispatches local-vs-remote
 * the same way as inspectStackServices.
 *
 *   - Local node  → reads the LOCAL filesystem via FileSystemService.getStacks().
 *   - Remote node → fetches `/api/mesh/local-stacks` against the resolved proxy
 *     target with the appropriate Authorization and license tier headers,
 *     parses the JSON envelope, and returns the decoded `stacks[]` array.
 *
 * Pre-fix the route in `routes/mesh.ts` called FileSystemService.getInstance(nodeId)
 * unconditionally, which always reads central's own filesystem regardless of
 * whether the targeted node was local or remote. Result: the mesh opt-in sheet
 * showed "No stacks deployed on this node yet" for every remote pilot.
 */
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
import { setupTestDb, cleanupTestDb } from './helpers/setupTestDb';

let tmpDir: string;
let MeshService: typeof import('../services/MeshService').MeshService;
let MeshError: typeof import('../services/MeshService').MeshError;
let DatabaseService: typeof import('../services/DatabaseService').DatabaseService;
let NodeRegistry: typeof import('../services/NodeRegistry').NodeRegistry;
let FileSystemService: typeof import('../services/FileSystemService').FileSystemService;

beforeAll(async () => {
    tmpDir = await setupTestDb();
    ({ MeshService, MeshError } = await import('../services/MeshService'));
    ({ DatabaseService } = await import('../services/DatabaseService'));
    ({ NodeRegistry } = await import('../services/NodeRegistry'));
    ({ FileSystemService } = await import('../services/FileSystemService'));
});

afterAll(() => {
    vi.restoreAllMocks();
    cleanupTestDb(tmpDir);
});

afterEach(() => {
    vi.restoreAllMocks();
});

describe('MeshService.listStacksOnNode dispatch (F8)', () => {
    it('uses the local filesystem path for the local node', async () => {
        const svc = MeshService.getInstance();
        const db = DatabaseService.getInstance();
        const localNodeId = db.getNodes()[0].id;

        const fsSpy = vi
            .spyOn(FileSystemService.prototype, 'getStacks')
            .mockResolvedValue(['audit-mesh-prod', 'whoami']);
        const fetchSpy = vi.spyOn(globalThis, 'fetch');

        const out = await svc.listStacksOnNode(localNodeId);

        expect(out).toEqual(['audit-mesh-prod', 'whoami']);
        expect(fsSpy).toHaveBeenCalledTimes(1);
        expect(fetchSpy).not.toHaveBeenCalled();
    });

    it('fetches /api/mesh/local-stacks for remote nodes and forwards the proxy target headers', async () => {
        const svc = MeshService.getInstance();
        const db = DatabaseService.getInstance();
        const remoteNodeId = db.addNode({
            name: 'list-stacks-remote-test',
            type: 'remote',
            mode: 'proxy',
            compose_dir: '/tmp',
            is_default: false,
            api_url: 'https://remote.example.com:1852',
            api_token: 'remote-tok',
        });

        vi.spyOn(NodeRegistry.getInstance(), 'getProxyTarget').mockReturnValue({
            apiUrl: 'https://remote.example.com:1852',
            apiToken: 'remote-tok',
            trustedLoopback: false,
        });

        const fetchMock = vi
            .spyOn(globalThis, 'fetch')
            .mockResolvedValue(new Response(
                JSON.stringify({ stacks: ['audit-mesh-pilot', 'monitor'] }),
                { status: 200, headers: { 'content-type': 'application/json' } },
            ));

        const out = await svc.listStacksOnNode(remoteNodeId);

        expect(out).toEqual(['audit-mesh-pilot', 'monitor']);
        expect(fetchMock).toHaveBeenCalledTimes(1);
        const call = fetchMock.mock.calls[0];
        expect(String(call[0])).toBe('https://remote.example.com:1852/api/mesh/local-stacks');
        const init = call[1] as { method: string; headers: Record<string, string> };
        expect(init.method).toBe('GET');
        expect(init.headers['Authorization']).toBe('Bearer remote-tok');
        expect(init.headers).toHaveProperty('x-sencho-tier');

        db.deleteNode(remoteNodeId);
    });

    it('returns [] when the remote responds non-2xx', async () => {
        const svc = MeshService.getInstance();
        const db = DatabaseService.getInstance();
        const remoteNodeId = db.addNode({
            name: 'list-stacks-remote-fail',
            type: 'remote',
            mode: 'proxy',
            compose_dir: '/tmp',
            is_default: false,
            api_url: 'https://remote.example.com:1852',
            api_token: 'remote-tok',
        });

        vi.spyOn(NodeRegistry.getInstance(), 'getProxyTarget').mockReturnValue({
            apiUrl: 'https://remote.example.com:1852',
            apiToken: 'remote-tok',
            trustedLoopback: false,
        });
        vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response('Internal Server Error', { status: 500 }));

        const out = await svc.listStacksOnNode(remoteNodeId);

        expect(out).toEqual([]);
        db.deleteNode(remoteNodeId);
    });

    it('returns [] for a remote node with no active proxy target (e.g. pilot-agent tunnel down)', async () => {
        const svc = MeshService.getInstance();
        const db = DatabaseService.getInstance();
        const remoteNodeId = db.addNode({
            name: 'list-stacks-remote-down',
            type: 'remote',
            mode: 'pilot_agent',
            compose_dir: '/tmp',
            is_default: false,
            api_url: '',
            api_token: '',
        });

        vi.spyOn(NodeRegistry.getInstance(), 'getProxyTarget').mockReturnValue(null);
        const fetchSpy = vi.spyOn(globalThis, 'fetch');

        const out = await svc.listStacksOnNode(remoteNodeId);

        expect(out).toEqual([]);
        expect(fetchSpy).not.toHaveBeenCalled();
        db.deleteNode(remoteNodeId);
    });

    it('defends against malformed remote bodies', async () => {
        const svc = MeshService.getInstance();
        const db = DatabaseService.getInstance();
        const remoteNodeId = db.addNode({
            name: 'list-stacks-remote-malformed',
            type: 'remote',
            mode: 'proxy',
            compose_dir: '/tmp',
            is_default: false,
            api_url: 'https://remote.example.com:1852',
            api_token: 'remote-tok',
        });

        vi.spyOn(NodeRegistry.getInstance(), 'getProxyTarget').mockReturnValue({
            apiUrl: 'https://remote.example.com:1852',
            apiToken: 'remote-tok',
            trustedLoopback: false,
        });
        vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(
            JSON.stringify({ stacks: ['ok-string', 42, null, { not: 'a string' }] }),
            { status: 200, headers: { 'content-type': 'application/json' } },
        ));

        const out = await svc.listStacksOnNode(remoteNodeId);

        expect(out).toEqual(['ok-string']);
        db.deleteNode(remoteNodeId);
    });

    it('returns [] for an unknown node id', async () => {
        const svc = MeshService.getInstance();
        const fetchSpy = vi.spyOn(globalThis, 'fetch');
        const out = await svc.listStacksOnNode(999_999);
        expect(out).toEqual([]);
        expect(fetchSpy).not.toHaveBeenCalled();
    });

    it('catch arm swallows MeshError(no_target) and logs the no-proxy-target warning', async () => {
        const svc = MeshService.getInstance();
        const db = DatabaseService.getInstance();
        const remoteNodeId = db.addNode({
            name: 'list-stacks-no-target-code',
            type: 'remote',
            mode: 'pilot_agent',
            compose_dir: '/tmp',
            is_default: false,
            api_url: '',
            api_token: '',
        });

        vi.spyOn(NodeRegistry.getInstance(), 'getProxyTarget').mockReturnValue(null);
        const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
        const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});

        const out = await svc.listStacksOnNode(remoteNodeId);

        expect(out).toEqual([]);
        expect(warnSpy).toHaveBeenCalledTimes(1);
        expect(String(warnSpy.mock.calls[0][0])).toContain('no proxy target for node');
        expect(errorSpy).not.toHaveBeenCalled();

        db.deleteNode(remoteNodeId);
    });

    it('unexpected MeshError codes fall through to the remote-unreachable error path', async () => {
        const svc = MeshService.getInstance();
        const db = DatabaseService.getInstance();
        const remoteNodeId = db.addNode({
            name: 'list-stacks-push-failed-falls-through',
            type: 'remote',
            mode: 'proxy',
            compose_dir: '/tmp',
            is_default: false,
            api_url: 'https://remote.example.com:1852',
            api_token: 'remote-tok',
        });

        vi.spyOn(NodeRegistry.getInstance(), 'getProxyTarget').mockReturnValue({
            apiUrl: 'https://remote.example.com:1852',
            apiToken: 'remote-tok',
            trustedLoopback: false,
        });
        vi.spyOn(globalThis, 'fetch').mockImplementation(() => {
            throw new MeshError('push_failed', 'simulated transport failure');
        });
        const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
        const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});

        const out = await svc.listStacksOnNode(remoteNodeId);

        expect(out).toEqual([]);
        expect(warnSpy).not.toHaveBeenCalled();
        expect(errorSpy).toHaveBeenCalledTimes(1);
        expect(String(errorSpy.mock.calls[0][0])).toContain('remote unreachable');

        db.deleteNode(remoteNodeId);
    });
});
