/**
 * Fail-closed approval columns on blueprints.
 * Seeds a pre-approval schema with an enabled Blueprint (and live placement),
 * then opens production DatabaseService startup so migrateAddBlueprintApproval runs.
 */
import { describe, it, expect, afterEach, vi } from 'vitest';
import fs from 'fs';
import os from 'os';
import path from 'path';
import Database from 'better-sqlite3';
import { DatabaseService } from '../services/DatabaseService';

const LEGACY_BLUEPRINT = 'legacy-web';

const APPROVAL_COLUMNS = [
    'approval_status',
    'approved_intent_fingerprint',
    'approved_blast_json',
    'approved_at',
    'approved_by',
] as const;

const ABSENT_BEFORE_MIGRATION = ['approval_status', 'approved_blast_json'] as const;

function resetDatabaseSingleton(): void {
    const holder = DatabaseService as unknown as { instance?: DatabaseService };
    const existing = holder.instance;
    if (existing) {
        try {
            existing.getDb().close();
        } catch {
            // already closed
        }
        holder.instance = undefined;
    }
}

type ApprovalRow = {
    approval_status: string | null;
    approved_intent_fingerprint: string | null;
    approved_blast_json: string | null;
    approved_at: number | null;
    approved_by: string | null;
    enabled: number;
};

function blueprintColumnNames(db: Database.Database): string[] {
    return (db.prepare('PRAGMA table_info(blueprints)').all() as Array<{ name: string }>).map(c => c.name);
}

function readApprovalRow(db: Database.Database, name: string): ApprovalRow {
    return db.prepare(
        `SELECT approval_status, approved_intent_fingerprint, approved_blast_json,
                approved_at, approved_by, enabled
         FROM blueprints WHERE name = ?`,
    ).get(name) as ApprovalRow;
}

function expectPendingNullAuth(row: ApprovalRow): void {
    expect(row.approval_status).toBe('pending');
    expect(row.approved_intent_fingerprint).toBeNull();
    expect(row.approved_blast_json).toBeNull();
    expect(row.approved_at).toBeNull();
    expect(row.approved_by).toBeNull();
    expect(row.enabled).toBe(1);
}

function seedLegacyPreApprovalSchema(dbPath: string, now: number): string[] {
    const seed = new Database(dbPath);
    try {
        // Legacy schema: no approval_* columns and no pinned_node_id.
        // Include a matching node + active deployment so a wrongly approved
        // migration would have something to mutate on reconcile.
        seed.exec(`
            CREATE TABLE nodes (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT NOT NULL UNIQUE,
                type TEXT NOT NULL DEFAULT 'local',
                compose_dir TEXT NOT NULL DEFAULT '/app/compose',
                is_default INTEGER DEFAULT 0,
                status TEXT NOT NULL DEFAULT 'unknown',
                created_at INTEGER NOT NULL
            );
            INSERT INTO nodes (id, name, type, compose_dir, is_default, status, created_at)
                VALUES (1, 'legacy-local', 'local', '/tmp/compose', 1, 'online', ${now});

            CREATE TABLE blueprints (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT NOT NULL UNIQUE,
                description TEXT,
                compose_content TEXT NOT NULL,
                selector_json TEXT NOT NULL,
                drift_mode TEXT NOT NULL DEFAULT 'suggest',
                classification TEXT NOT NULL DEFAULT 'unknown',
                classification_reasons TEXT,
                enabled INTEGER NOT NULL DEFAULT 1,
                revision INTEGER NOT NULL DEFAULT 1,
                created_at INTEGER NOT NULL,
                updated_at INTEGER NOT NULL,
                created_by TEXT
            );
            INSERT INTO blueprints (
                name, description, compose_content, selector_json, drift_mode,
                classification, classification_reasons, enabled, revision,
                created_at, updated_at, created_by
            ) VALUES (
                '${LEGACY_BLUEPRINT}',
                NULL,
                'services:\n  app:\n    image: nginx\n',
                '{"type":"nodes","ids":[1]}',
                'observe',
                'stateless',
                '[]',
                1,
                1,
                ${now},
                ${now},
                'admin'
            );

            CREATE TABLE blueprint_deployments (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                blueprint_id INTEGER NOT NULL,
                node_id INTEGER NOT NULL,
                status TEXT NOT NULL,
                applied_revision INTEGER,
                last_deployed_at INTEGER,
                last_checked_at INTEGER,
                last_drift_at INTEGER,
                drift_summary TEXT,
                last_error TEXT,
                UNIQUE(blueprint_id, node_id)
            );
            INSERT INTO blueprint_deployments
                (blueprint_id, node_id, status, applied_revision, last_deployed_at)
                VALUES (1, 1, 'active', 1, ${now});
        `);
        return blueprintColumnNames(seed);
    } finally {
        seed.close();
    }
}

function bootDatabaseService(scratchDir: string): DatabaseService {
    process.env.DATA_DIR = scratchDir;
    process.env.COMPOSE_DIR = path.join(scratchDir, 'compose');
    fs.mkdirSync(process.env.COMPOSE_DIR, { recursive: true });
    resetDatabaseSingleton();
    return DatabaseService.getInstance();
}

describe('blueprint approval column migration', () => {
    let scratchDir: string | null = null;

    afterEach(() => {
        vi.restoreAllMocks();
        resetDatabaseSingleton();
        if (scratchDir) {
            try {
                fs.rmSync(scratchDir, { recursive: true, force: true });
            } catch {
                // best-effort
            }
            scratchDir = null;
        }
    });

    it('migrates a pre-approval enabled blueprint to pending with no authorization material', async () => {
        scratchDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sencho-bp-approval-mig-'));
        const now = Date.now();
        const colNamesBefore = seedLegacyPreApprovalSchema(path.join(scratchDir, 'sencho.db'), now);
        for (const absent of ABSENT_BEFORE_MIGRATION) {
            expect(colNamesBefore).not.toContain(absent);
        }

        const db = bootDatabaseService(scratchDir);

        for (const required of APPROVAL_COLUMNS) {
            expect(blueprintColumnNames(db.getDb())).toContain(required);
        }

        // Assert raw SQLite values (not parseBlueprint coercion).
        expectPendingNullAuth(readApprovalRow(db.getDb(), LEGACY_BLUEPRINT));

        const row = db.getBlueprintByName(LEGACY_BLUEPRINT);
        expect(row).toBeTruthy();
        expect(row!.id).toBe(1);
        expect(db.getNodes().some(n => n.id === 1)).toBe(true);
        expect(db.listDeployments(1).some(d => d.node_id === 1 && d.status === 'active')).toBe(true);

        const { BlueprintService } = await import('../services/BlueprintService');
        const { BlueprintReconciler } = await import('../services/BlueprintReconciler');
        const deploySpy = vi.spyOn(BlueprintService.getInstance(), 'deployToNode').mockResolvedValue({ status: 'active' });
        const withdrawSpy = vi.spyOn(BlueprintService.getInstance(), 'withdrawFromNode').mockResolvedValue({ status: 'withdrawn' });
        const upsertSpy = vi.spyOn(db, 'upsertDeployment');

        await BlueprintReconciler.getInstance().reconcileOne(row!.id);

        expect(deploySpy).not.toHaveBeenCalled();
        expect(withdrawSpy).not.toHaveBeenCalled();
        expect(upsertSpy).not.toHaveBeenCalled();

        // Idempotent reopen keeps a single set of columns and pending row state.
        resetDatabaseSingleton();
        process.env.DATA_DIR = scratchDir;
        const db2 = DatabaseService.getInstance();
        expect(blueprintColumnNames(db2.getDb()).filter(name => name === 'approval_status')).toHaveLength(1);
        expectPendingNullAuth(readApprovalRow(db2.getDb(), LEGACY_BLUEPRINT));
    });
});
