/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */

import test from 'node:test';
import assert from 'node:assert/strict';
import { validateScriptPreflight, validateScriptPreflightDetailed } from './script-preflight.js';

test('preflight accepts valid dedicated create methods', () => {
  const code = `
const h = bim.create.project({ Name: "Schema Coverage" });
const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
bim.create.addIfcWall(h, s0, {
  Name: "Wall",
  Start: [0, 0, 0],
  End: [5, 0, 0],
  Thickness: 0.2,
  Height: 3,
});
bim.create.addIfcSlab(h, s0, {
  Name: "Slab",
  Position: [0, 0, 0],
  Width: 5,
  Depth: 4,
  Thickness: 0.3,
});
`;

  const errors = validateScriptPreflight(code);
  assert.deepEqual(errors, []);
});

test('preflight suggests nearest method names for typos', () => {
  const code = `
const h = bim.create.project({ Name: "Typo" });
const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
bim.create.addIfcWal(h, s0, { Start: [0, 0, 0], End: [1, 0, 0], Thickness: 0.2, Height: 3 });
`;

  const errors = validateScriptPreflight(code);
  assert.equal(errors.length, 1);
  assert.match(errors[0], /Did you mean `bim\.create\.addIfcWall\(\)`\?/);
});

test('preflight warns when standalone windows are used with walls but no wall openings exist', () => {
  const code = `
const h = bim.create.project({ Name: "Window House" });
const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
bim.create.addIfcWall(h, s0, {
  Start: [0, 0, 0],
  End: [5, 0, 0],
  Thickness: 0.2,
  Height: 3,
});
bim.create.addIfcWindow(h, s0, {
  Position: [2.5, 0, 1.0],
  Width: 1.2,
  Height: 1.2,
});
`;
  const errors = validateScriptPreflight(code);
  assert.ok(errors.some((error) => error.includes('world-aligned standalone window')));
  assert.ok(errors.some((error) => error.includes('addIfcWallWindow')));
});

test('preflight accepts wall openings without standalone windows', () => {
  const code = `
const h = bim.create.project({ Name: "Window House" });
const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
bim.create.addIfcWall(h, s0, {
  Start: [0, 0, 0],
  End: [5, 0, 0],
  Thickness: 0.2,
  Height: 3,
  Openings: [
    { Width: 1.2, Height: 1.2, Position: [2.5, 0, 1.0] },
  ],
});
`;
  const errors = validateScriptPreflight(code);
  assert.deepEqual(errors, []);
});

test('preflight accepts hosted wall window helper usage', () => {
  const code = `
const h = bim.create.project({ Name: "Window House" });
const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
const wall = bim.create.addIfcWall(h, s0, {
  Start: [0, 0, 0],
  End: [5, 0, 0],
  Thickness: 0.2,
  Height: 3,
});
bim.create.addIfcWallWindow(h, wall, {
  Position: [2.5, 0, 1.0],
  Width: 1.2,
  Height: 1.2,
});
`;
  const errors = validateScriptPreflight(code);
  assert.deepEqual(errors, []);
});

test('preflight emits one targeted diagnostic per standalone opening call', () => {
  const diagnostics = validateScriptPreflightDetailed(`
const h = bim.create.project({ Name: "Window House" });
const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
bim.create.addIfcWall(h, s0, {
  Start: [0, 0, 0],
  End: [5, 0, 0],
  Thickness: 0.2,
  Height: 3,
});
bim.create.addIfcDoor(h, s0, {
  Position: [1.0, 0, 0],
  Width: 1.0,
  Height: 2.1,
});
bim.create.addIfcWindow(h, s0, {
  Position: [2.5, 0, 1.0],
  Width: 1.2,
  Height: 1.2,
});
`);

  const openingDiagnostics = diagnostics.filter((diagnostic) => diagnostic.code === 'wall_hosted_opening_pattern');
  assert.equal(openingDiagnostics.length, 2);
  assert.deepEqual(
    openingDiagnostics.map((diagnostic) => diagnostic.data?.methodName).sort(),
    ['addIfcDoor', 'addIfcWindow'],
  );
  assert.ok(openingDiagnostics.every((diagnostic) => diagnostic.rootCauseKey === 'placement_context_mismatch'));
  assert.ok(openingDiagnostics.every((diagnostic) => diagnostic.repairScope === 'block'));
  assert.ok(openingDiagnostics.every((diagnostic) => typeof diagnostic.data?.snippet === 'string'));
  assert.ok(openingDiagnostics.every((diagnostic) => diagnostic.data?.range));
});

test('preflight can target an unterminated standalone opening fragment', () => {
  const diagnostics = validateScriptPreflightDetailed(`
const h = bim.create.project({ Name: "Broken" });
const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
bim.create.addIfcWall(h, s0, {
  Start: [0, 0, 0],
  End: [5, 0, 0],
  Thickness: 0.2,
  Height: 3,
});
bim.create.addIfcDoor(h, s0, {
  Name: "Front Door",
// truncated repair debris
`);

  const openingDiagnostic = diagnostics.find((diagnostic) => diagnostic.code === 'wall_hosted_opening_pattern');
  assert.equal(openingDiagnostic?.data?.methodName, 'addIfcDoor');
  assert.equal(openingDiagnostic?.data?.unterminated, true);
  assert.match(String(openingDiagnostic?.data?.snippet ?? ''), /addIfcDoor/);
});

test('preflight rejects addIfcPlate slab-style contract misuse', () => {
  const code = `
const h = bim.create.project({ Name: "Facade" });
const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
bim.create.addIfcPlate(h, s0, {
  Position: [0, 0, 0],
  Width: 2.8,
  Height: 3.0,
  Thickness: 0.08,
});
`;
  const errors = validateScriptPreflight(code);
  assert.ok(errors.some((error) => error.includes('uses `Depth` and `Thickness`, not `Height`')));
  assert.ok(errors.some((error) => error.includes('missing required key(s): `Depth`')));
});

test('preflight warns when repeated world-placement methods stay at ground level in a storey loop', () => {
  const code = `
const h = bim.create.project({ Name: "Tower" });
const storeyHeight = 3.5;
const storeyCount = 10;
for (let i = 0; i < storeyCount; i++) {
  const elevation = i * storeyHeight;
  const storey = bim.create.addIfcBuildingStorey(h, { Name: "Level " + i, Elevation: elevation });
  bim.create.addIfcCurtainWall(h, storey, {
    Start: [0, -0.2, 0],
    End: [30, -0.2, 0],
    Height: storeyHeight,
    Thickness: 0.15,
  });
}
`;
  const errors = validateScriptPreflight(code);
  assert.ok(errors.some((error) => error.includes('Suspicious multi-level placement')));
});

test('preflight warns when material is queried via property sets', () => {
  const code = `
const entities = bim.query.all();
for (const entity of entities) {
  const material = bim.query.property(entity, "Pset_MaterialCommon", "Material")
    ?? bim.query.property(entity, "Material", "Name");
  console.log(material);
}
`;
  const errors = validateScriptPreflight(code);
  assert.ok(errors.some((error) => error.includes('Prefer `bim.query.materials(entity)`')));
});

test('preflight returns structured diagnostics with codes', () => {
  const diagnostics = validateScriptPreflightDetailed(`
const entities = bim.query.all();
for (const entity of entities) {
  const material = bim.query.property(entity, "Pset_MaterialCommon", "Material");
  console.log(material);
}
`);

  assert.ok(diagnostics.some((diagnostic) => diagnostic.code === 'metadata_query_pattern'));
  assert.ok(diagnostics.some((diagnostic) => diagnostic.source === 'preflight'));
});

test('preflight diagnostics include method metadata when available', () => {
  const diagnostics = validateScriptPreflightDetailed(`
const h = bim.create.project({ Name: "Facade" });
const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
bim.create.addIfcPlate(h, s0, {
  Position: [0, 0, 0],
  Width: 2.8,
  Height: 3.0,
  Thickness: 0.08,
});
`);

  const contractDiagnostic = diagnostics.find((diagnostic) => diagnostic.code === 'create_contract');
  assert.equal(contractDiagnostic?.data?.methodName, 'addIfcPlate');
});

test('preflight warns on detached snippet scope for common facade variables', () => {
  const code = `
for (let x = 0; x < width; x += 3) {
  bim.create.addIfcMember(h, storey, {
    Start: [x, -0.2, z],
    End: [x, -0.2, z + 3.5],
    Width: 0.25,
    Height: 0.15,
  });
}
`;
  const errors = validateScriptPreflight(code);
  assert.ok(errors.some((error) => error.includes('reference `h`')));
  assert.ok(errors.some((error) => error.includes('reference `storey`')));
  assert.ok(errors.some((error) => error.includes('references `width`')));
  assert.ok(errors.some((error) => error.includes('references `z`')));
});

test('preflight detached snippet diagnostics carry structural repair metadata and evidence', () => {
  const diagnostics = validateScriptPreflightDetailed(`
for (let x = 0; x < width; x += 3) {
  bim.create.addIfcMember(h, storey, {
    Start: [x, -0.2, z],
    End: [x, -0.2, z + 3.5],
    Width: 0.25,
    Height: 0.15,
  });
}
`);

  const detached = diagnostics.filter((diagnostic) => diagnostic.code === 'detached_snippet_scope');
  assert.ok(detached.length >= 3);
  assert.ok(detached.every((diagnostic) => diagnostic.rootCauseKey === 'detached_fragment_rewrite'));
  assert.ok(detached.every((diagnostic) => diagnostic.repairScope === 'structural'));
  assert.ok(detached.every((diagnostic) => diagnostic.evidence?.length));
});

test('preflight does not flag destructured loop bindings in complete rewrite scripts as detached snippets', () => {
  const diagnostics = validateScriptPreflightDetailed(`
const h = bim.create.project({ Name: "3-Story House" });

const storyHeight = 3.0;
const floorThickness = 0.3;
const wallThickness = 0.25;
const roofThickness = 0.25;
const windowWidth = 1.2;
const windowHeight = 1.5;
const windowSillHeight = 0.9;

const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Ground Floor", Elevation: 0 });
const s1 = bim.create.addIfcBuildingStorey(h, { Name: "First Floor", Elevation: storyHeight });
const s2 = bim.create.addIfcBuildingStorey(h, { Name: "Second Floor", Elevation: storyHeight * 2 });

for (const [i, storey] of [s0, s1, s2].entries()) {
  bim.create.addIfcWall(h, storey, {
    Name: "North Wall Level " + i,
    Start: [0, 0, 0],
    End: [10, 0, 0],
    Height: storyHeight,
    Thickness: wallThickness,
    Openings: [
      {
        Position: [2.5, 0, windowSillHeight],
        Width: windowWidth,
        Height: windowHeight
      }
    ]
  });
}

bim.create.addIfcGableRoof(h, s2, {
  Name: "Gable Roof",
  Position: [0, 0, storyHeight],
  Width: 10,
  Depth: 8,
  Thickness: roofThickness,
  Slope: 30 * Math.PI / 180,
  Overhang: 0.5
});

const result = bim.create.toIfc(h);
bim.model.loadIfc(result.content, "3-story-house-with-windows.ifc");
`);

  const detached = diagnostics.filter((diagnostic) => diagnostic.code === 'detached_snippet_scope');
  assert.equal(detached.length, 0);
});

test('preflight does not flag object destructuring or arrow params as detached snippets', () => {
  const diagnostics = validateScriptPreflightDetailed(`
const h = bim.create.project({ Name: "Facade" });
const storey = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
const footprint = { width: 10, depth: 8 };
const { width, depth } = footprint;
const buildWall = (z) => {
  bim.create.addIfcWall(h, storey, {
    Start: [0, 0, z],
    End: [width, 0, z],
    Height: 3,
    Thickness: 0.25,
  });
};
buildWall(0);

const result = bim.create.toIfc(h);
bim.model.loadIfc(result.content, "facade.ifc");
`);

  const detached = diagnostics.filter((diagnostic) => diagnostic.code === 'detached_snippet_scope');
  assert.equal(detached.length, 0);
});

test('preflight rejects unsupported rotation keys on windows', () => {
  const code = `
const h = bim.create.project({ Name: "Rotated Window" });
const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
bim.create.addIfcWindow(h, s0, {
  Position: [2.5, 0, 1.0],
  Width: 1.2,
  Height: 1.2,
  Rotation: Math.PI / 2,
});
`;
  const errors = validateScriptPreflight(code);
  assert.ok(errors.some((error) => error.includes('does not support rotation')));
});

test('preflight rejects unsupported rotation keys on hosted wall windows', () => {
  const code = `
const h = bim.create.project({ Name: "Rotated Hosted Window" });
const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
const wall = bim.create.addIfcWall(h, s0, {
  Start: [0, 0, 0],
  End: [5, 0, 0],
  Thickness: 0.2,
  Height: 3,
});
bim.create.addIfcWallWindow(h, wall, {
  Position: [2.5, 0, 1.0],
  Width: 1.2,
  Height: 1.2,
  Rotation: Math.PI / 2,
});
`;
  const errors = validateScriptPreflight(code);
  assert.ok(errors.some((error) => error.includes('auto-aligns to the host wall')));
});

test('preflight rejects invalid addElement contract shape', () => {
  const code = `
const h = bim.create.project({ Name: "Bad Generic" });
const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
bim.create.addElement(h, s0, {
  Type: "IfcBuildingElementProxy",
  Position: [0, 0, 0],
  Profile: { kind: "rect", xDim: 1, yDim: 1 },
  Height: 3,
});
`;
  const errors = validateScriptPreflight(code);
  assert.ok(errors.some((error) => error.includes('uses `IfcType`, not `Type`')));
  assert.ok(errors.some((error) => error.includes('uses `Placement: { Location: [...] }`, not `Position`')));
  assert.ok(errors.some((error) => error.includes('uses `Depth`, not `Height`')));
  assert.ok(errors.some((error) => error.includes('ProfileType')));
});

test('preflight rejects unsupported roof profile geometry', () => {
  const code = `
const h = bim.create.project({ Name: "Roof" });
const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
bim.create.addIfcRoof(h, s0, {
  Name: "Bad Roof",
  Position: [0, 0, 3],
  Profile: [[0, 0], [1, 0], [1, 1]],
  Thickness: 0.2,
  Width: 10,
  Depth: 8,
});
`;
  const errors = validateScriptPreflight(code);
  assert.ok(errors.some((error) => error.includes('does not support `Profile`')));
});

test('preflight rejects roof slopes that look like degrees', () => {
  const code = `
const h = bim.create.project({ Name: "Roof Degrees" });
const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
bim.create.addIfcRoof(h, s0, {
  Name: "Bad Roof",
  Position: [0, 0, 3],
  Width: 10,
  Depth: 8,
  Thickness: 0.2,
  Slope: 15,
});
`;
  const errors = validateScriptPreflight(code);
  assert.ok(errors.some((error) => error.includes('convert them first')));
});

test('preflight steers gable intent to addIfcGableRoof', () => {
  const code = `
const h = bim.create.project({ Name: "Gable" });
const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
bim.create.addIfcRoof(h, s0, {
  Name: "Main Gable Roof",
  Position: [0, 0, 3],
  Width: 10,
  Depth: 8,
  Thickness: 0.2,
  Slope: Math.PI / 12,
});
`;
  const errors = validateScriptPreflight(code);
  assert.ok(errors.some((error) => error.includes('addIfcGableRoof')));
});

test('preflight accepts valid gable roof helper usage', () => {
  const code = `
const h = bim.create.project({ Name: "Gable" });
const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
bim.create.addIfcGableRoof(h, s0, {
  Name: "Main Roof",
  Position: [0, 0, 3],
  Width: 10,
  Depth: 8,
  Thickness: 0.2,
  Slope: Math.PI / 12,
  Overhang: 0.3,
});
`;
  const errors = validateScriptPreflight(code);
  assert.deepEqual(errors, []);
});

test('preflight rejects slab missing footprint and profile', () => {
  const code = `
const h = bim.create.project({ Name: "Slab" });
const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
bim.create.addIfcSlab(h, s0, {
  Position: [0, 0, 0],
  Thickness: 0.3,
});
`;
  const errors = validateScriptPreflight(code);
  assert.ok(errors.some((error) => error.includes('requires one of')));
});

test('preflight rejects zero-length axis geometry', () => {
  const code = `
const h = bim.create.project({ Name: "Axis" });
const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
bim.create.addIfcWall(h, s0, {
  Start: [0, 0, 0],
  End: [0, 0, 0],
  Thickness: 0.2,
  Height: 3,
});
`;
  const errors = validateScriptPreflight(code);
  assert.ok(errors.some((error) => error.includes('non-zero axis')));
});

test('preflight rejects suspicious bare identifier values', () => {
  const code = `
const h = bim.create.project({ Name: "Bare" });
const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
bim.create.addIfcColumn(h, s0, {
  Position: Position,
  Width: 0.4,
  Depth: 0.4,
  Height: 3,
});
`;
  const errors = validateScriptPreflight(code);
  assert.ok(errors.some((error) => error.includes('Suspicious bare identifier value `Position`')));
});
