import { adminUserId, load } from './setup.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';

const {
  buildWin32LobAppPayload,
  buildAssignmentsPayload,
  buildRelationshipsPayload,
  validateRelationships,
  MAX_SUPERSEDENCE_TARGETS
} = await load('../services/intunePublishService.js');
const { createGraphConnection, recordGraphAudit, listGraphAudit } = await load('../models/graphModel.js');

const owner = adminUserId();

function deploymentFixture(overrides = {}) {
  return {
    id: 'intune_fixture',
    name: 'Contoso VPN',
    notes: 'Pilot ring',
    intunewinFile: 'ContosoVpn.intunewin',
    installBehavior: 'system',
    restartBehavior: 'return-code',
    installCommand: 'Invoke-AppDeployToolkit.exe -DeploymentType Install',
    uninstallCommand: 'Invoke-AppDeployToolkit.exe -DeploymentType Uninstall',
    detectionType: 'msi-product-code',
    detectionRule: '{12345678-1234-1234-1234-123456789012}',
    returnCodes: [{ code: 3010, type: 'softReboot' }, { code: 1602, type: 'retry' }],
    requirements: { architecture: 'both', minOs: 'Windows 10 22H2', diskSpaceMb: 512, runAs32Bit: false },
    ...overrides
  };
}

const profileFixture = { appVendor: 'Contoso', appName: 'VPN Client', appVersion: '3.1.0' };

test('builds a win32LobApp payload with correct core shape', () => {
  const { payload } = buildWin32LobAppPayload(deploymentFixture(), profileFixture);
  assert.equal(payload['@odata.type'], '#microsoft.graph.win32LobApp');
  assert.equal(payload.displayName, 'Contoso VPN Client 3.1.0');
  assert.equal(payload.publisher, 'Contoso');
  assert.equal(payload.installExperience.runAsAccount, 'system');
  assert.equal(payload.installExperience.deviceRestartBehavior, 'basedOnReturnCode');
  assert.equal(payload.applicableArchitectures, 'x64,x86');
  assert.equal(payload.minimumSupportedOperatingSystem.v10_22H2, true);
});

test('return codes always include 0=success and clamp unknown types', () => {
  const { payload } = buildWin32LobAppPayload(deploymentFixture({ returnCodes: [{ code: 7, type: 'bogus' }] }));
  assert.ok(payload.returnCodes.some((rc) => rc.returnCode === 0 && rc.type === 'success'));
  assert.equal(payload.returnCodes.find((rc) => rc.returnCode === 7).type, 'failed');
});

test('MSI detection maps to product code rule', () => {
  const { payload } = buildWin32LobAppPayload(deploymentFixture());
  const rule = payload.detectionRules[0];
  assert.equal(rule['@odata.type'], '#microsoft.graph.win32LobAppProductCodeDetection');
  assert.equal(rule.productCode, '{12345678-1234-1234-1234-123456789012}');
});

test('custom-script detection base64-encodes the script body', () => {
  const { payload } = buildWin32LobAppPayload(deploymentFixture({ detectionType: 'custom-script', detectionRule: 'Test-Path C:\\app.exe' }));
  const rule = payload.detectionRules[0];
  assert.equal(rule['@odata.type'], '#microsoft.graph.win32LobAppPowerShellScriptDetection');
  assert.equal(Buffer.from(rule.scriptContent, 'base64').toString('utf8'), 'Test-Path C:\\app.exe');
});

test('missing .intunewin produces a content warning', () => {
  const { warnings } = buildWin32LobAppPayload(deploymentFixture({ intunewinFile: '' }));
  assert.ok(warnings.some((w) => w.toLowerCase().includes('content must be uploaded')));
});

// --- Phase 3: assignments -------------------------------------------------

test('group assignment maps to groupAssignmentTarget with intent', () => {
  const { assignments } = buildAssignmentsPayload({
    assignments: [{ ring: 'Broad', intent: 'required', groupMode: 'group', groupId: 'g-1' }]
  });
  assert.equal(assignments.length, 1);
  assert.equal(assignments[0].intent, 'required');
  assert.equal(assignments[0].target['@odata.type'], '#microsoft.graph.groupAssignmentTarget');
  assert.equal(assignments[0].target.groupId, 'g-1');
});

test('exclude intent maps to exclusionGroupAssignmentTarget', () => {
  const { assignments } = buildAssignmentsPayload({
    assignments: [{ ring: 'Exclusions', intent: 'exclude', groupMode: 'group', groupId: 'g-x' }]
  });
  assert.equal(assignments[0].target['@odata.type'], '#microsoft.graph.exclusionGroupAssignmentTarget');
  assert.ok(['required', 'available', 'uninstall'].includes(assignments[0].intent));
});

test('all-devices target and assignment filters are honored', () => {
  const { assignments } = buildAssignmentsPayload({
    assignments: [{ ring: 'AllDev', intent: 'available', groupMode: 'allDevices', filterId: 'f-1', filterType: 'exclude', autoUpdate: true }]
  });
  assert.equal(assignments[0].target['@odata.type'], '#microsoft.graph.allDevicesAssignmentTarget');
  assert.equal(assignments[0].target.deviceAndAppManagementAssignmentFilterId, 'f-1');
  assert.equal(assignments[0].target.deviceAndAppManagementAssignmentFilterType, 'exclude');
  assert.equal(assignments[0].settings.autoUpdateSettings.autoUpdateSupersededApps, 'enabled');
});

test('group assignment without an object id is skipped with a warning', () => {
  const { assignments, warnings } = buildAssignmentsPayload({
    assignments: [{ ring: 'Broad', intent: 'required', groupMode: 'group', groupId: '' }]
  });
  assert.equal(assignments.length, 0);
  assert.ok(warnings.some((w) => w.includes('Broad')));
});

// --- Phase 4: relationships ----------------------------------------------

test('supersedence and dependency relationships map to correct Graph types', () => {
  const { relationships } = buildRelationshipsPayload([
    { kind: 'supersedence', targetAppId: 'old-app', mode: 'replace' },
    { kind: 'dependency', targetAppId: 'dep-app', mode: 'autoInstall' }
  ], 'self-app');
  assert.equal(relationships[0]['@odata.type'], '#microsoft.graph.mobileAppSupersedence');
  assert.equal(relationships[0].supersedenceType, 'replace');
  assert.equal(relationships[1]['@odata.type'], '#microsoft.graph.mobileAppDependency');
  assert.equal(relationships[1].dependencyType, 'autoInstall');
});

test('relationship validation rejects self-reference and duplicates', () => {
  const errors = validateRelationships([
    { kind: 'supersedence', targetAppId: 'self' },
    { kind: 'supersedence', targetAppId: 'a' },
    { kind: 'supersedence', targetAppId: 'a' }
  ], 'self');
  assert.ok(errors.some((e) => e.includes('itself')));
  assert.ok(errors.some((e) => e.toLowerCase().includes('duplicate')));
});

test('relationship validation enforces the supersedence node ceiling', () => {
  const many = Array.from({ length: MAX_SUPERSEDENCE_TARGETS + 1 }, (_, i) => ({ kind: 'supersedence', targetAppId: `app-${i}` }));
  const errors = validateRelationships(many, 'self');
  assert.ok(errors.some((e) => e.includes('Too many supersedence')));
  assert.throws(() => buildRelationshipsPayload(many, 'self'));
});

test('graph connection persists the allow_write write-gate', () => {
  const conn = createGraphConnection({
    name: 'WriteConn', tenantId: 'contoso', clientId: 'abc', clientSecret: 'shh',
    allowWrite: true, visibility: 'personal'
  }, owner);
  assert.equal(conn.allowWrite, true);
});

test('audit log records and reads back request/response payloads', () => {
  // connection/deployment are nullable FKs; use null here and assert the
  // payload round-trips. (Real publishes pass real ids.)
  recordGraphAudit({
    connectionId: null, deploymentId: null, actorUserId: owner, actorEmail: 'a@b.c',
    action: 'win32app.create', targetGraphId: 'app123', status: 'success', message: 'ok',
    request: { displayName: 'X' }, response: { id: 'app123' }
  });
  const rows = listGraphAudit({ limit: 5 });
  const entry = rows.find((r) => r.targetGraphId === 'app123');
  assert.ok(entry, 'audit entry should be retrievable');
  assert.equal(entry.action, 'win32app.create');
  assert.equal(entry.request.displayName, 'X');
  assert.equal(entry.response.id, 'app123');
});
