// Domain: authority (spans safety -- the gates here refuse before any byte
// reaches disk, which is also the safety surface).
const test = require('node:test');
const { describe } = require('node:test');
const assert = require('node:assert/strict');
const cp = require('node:child_process');
const crypto = require('node:crypto');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');

const { binding } = require('../../test-support/snapshot');
const scripts = path.join(__dirname, '..', '..', 'scripts');

describe('bounded write authority: mode, scope, ownership, and evidence gates', () => {
  const runtime = require(path.join(scripts, 'lib', 'runtime'));
  const services = require(path.join(scripts, 'lib', 'services'));

  // #197: `okf_version`/`project_mode` live in the manifest's selected bundle
  // record now, not `index.md`'s frontmatter.
  function setManifest(root, { okfVersion = '0.2', projectMode = 'knowledge-only' } = {}) {
    fs.writeFileSync(path.join(root, '.okf-workspace.json'), JSON.stringify({
      schema_version: 1,
      workspace_id: '3f8c1b2e-4a5d-4e6f-8a9b-0c1d2e3f4a5b',
      repositories: [{ name: 'repo', path: '.', local: true }],
      bundles: [{ alias: 'repo', owner: 'repo', root: '.', okf_version: okfVersion, project_mode: projectMode }],
    }));
  }

  function bundle(t, mode = 'knowledge-only') {
    const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'okf-53-')));
    t.after(() => fs.rmSync(root, { recursive: true, force: true }));
    fs.mkdirSync(path.join(root, '.git'));
    setManifest(root, { projectMode: mode });
    fs.writeFileSync(path.join(root, 'index.md'), '# Bundle\n');
    fs.writeFileSync(path.join(root, 'evidence.md'), 'observed evidence\n');
    return root;
  }

  function concept(root, name = 'note.md') {
    fs.writeFileSync(path.join(root, name), '---\ntype: Note\ntitle: Before\n---\n# Note\n');
  }

  function request(root, skill, operation, extra = {}) {
    return {
      protocol: 'okf-wrapper/1',
      skill,
      operation,
      task_kind: 'fix',
      scope: { concepts: ['note.md'] },
      invocation: 'explicit',
      payload: {
        cwd: root,
        bundle: root,
        concept: 'note.md',
        set: { title: 'After' },
        evidence: [binding(root, 'evidence.md')],
        ...extra,
      },
    };
  }

  function run(value) {
    const result = cp.spawnSync(process.execPath, [path.join(scripts, `${value.skill}.js`)], {
      input: JSON.stringify(value), encoding: 'utf8',
    });
    assert.equal(result.status, 0, result.stderr);
    assert.equal(result.stderr, '');
    return JSON.parse(result.stdout);
  }

  function digest(root) {
    const hash = crypto.createHash('sha256');
    function visit(directory, relative = '') {
      for (const entry of fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
        const file = path.join(directory, entry.name);
        const name = path.join(relative, entry.name);
        hash.update(name).update(entry.isDirectory() ? 'directory' : 'file');
        if (entry.isDirectory()) visit(file, name);
        else hash.update(fs.readFileSync(file));
      }
    }
    visit(root);
    return hash.digest('hex');
  }

  test('writer applies a bounded evidence-backed revision and reports its atomic outcome', (t) => {
    const root = bundle(t);
    concept(root);
    const response = run(request(root, 'okf-write', 'revise'));
    assert.equal(response.result, 'applied');
    assert.equal(response.data.authorization, 'notice');
    assert.deepEqual(response.data.evidence, [binding(root, 'evidence.md')]);
    assert.deepEqual(response.data.effects.map((item) => item.effect), ['concept-revise']);
    assert.deepEqual(response.evidence_limits, {
      writes: 'not serialized',
      crash_recovery: 'not provided',
      semantic_support: 'not runtime-verified',
      non_file_evidence: 'accepted proposal only',
    });
    assert.match(fs.readFileSync(path.join(root, 'note.md'), 'utf8'), /title: After/);
  });

  // #197: `root version`/`project mode` used to inject a mid-flight `index.md`
  // corruption between the write and `postWrite`'s re-check to prove the
  // re-check reads the file fresh rather than trusting the pre-write value.
  // `okf_version`/`project_mode` now come from the manifest bundle record
  // resolved once before the write and reused unchanged for the re-check (never
  // re-read from either file), so there is no longer a file whose mid-flight
  // mutation this scenario could observe -- it is unreachable the same way
  // `test/conformance/*` documents for the same two checks. `link`/`upstream`
  // are untouched: `checkLinks`/`checkUpstreams` still read the bundle tree
  // fresh in `postWrite`.
  test('writer rechecks the saved bundle through its filesystem service', (t) => {
    const cases = [
      ['link', (root) => fs.rmSync(path.join(root, 'source.md')), 'applied', 'UNRESOLVED_INTERNAL_LINK'],
      ['upstream', (root) => fs.writeFileSync(path.join(root, 'source.md'), '---\ntitle: Source\n---\n# Source\n'), 'failed/incomplete', 'DEPENDS_ON_BLOCKED_CONCEPT'],
    ];

    for (const [label, change, result, code] of cases) {
      const root = bundle(t);
      concept(root);
      fs.writeFileSync(path.join(root, 'source.md'), '---\ntype: Note\n---\n# Source\n');
      const response = runtime.run('okf-write', request(root, 'okf-write', 'revise', {
        set: { title: 'After', sources: [{ resource: 'source.md' }] },
      }), {
        ...services,
        publishFile(file, data, expected) {
          services.publishFile(file, data, expected);
          change(root);
        },
      });
      assert.equal(response.result, result, label);
      assert.ok(response.findings.some((finding) => finding.code === code), label);
    }
  });

  test('direct scope is inferred and requested derivatives append to a frontmatter-free log', (t) => {
    const root = bundle(t);
    concept(root);
    fs.writeFileSync(path.join(root, 'log.md'), '# Log\n');
    const value = request(root, 'okf-write', 'revise', {
      effects: ['concept-revise', 'index-maintenance', 'log-append'],
    });
    delete value.scope;
    const response = run(value);
    assert.deepEqual(response.scope, { concepts: ['note.md'] });
    assert.deepEqual(response.data.effects.map((item) => item.inherited), [false, true, true]);
    assert.match(fs.readFileSync(path.join(root, 'index.md'), 'utf8'), /- \[note.md\]\(note.md\)/);
    assert.match(fs.readFileSync(path.join(root, 'log.md'), 'utf8'), /- revise: \[note.md\]\(note.md\)/);
  });

  test('writer refuses unsupported composites before writing', (t) => {
    const root = bundle(t);
    concept(root);
    const before = digest(root);
    const response = run(request(root, 'okf-write', 'revise', { effects: ['concept-revise', 'delete'] }));
    assert.equal(response.result, 'blocked');
    assert.equal(response.data.code, 'UNSUPPORTED_INPUT');
    assert.ok(response.findings.some((item) => item.code === 'UNSUPPORTED_INPUT' && item.blocks));
    assert.equal(digest(root), before);
  });

  test('lifecycle plans a narrow sync through the writer and automatic requests do not write', (t) => {
    const root = bundle(t);
    concept(root);
    const sync = request(root, 'okf-lifecycle', 'sync');
    const response = run(sync);
    assert.equal(response.result, 'applied');
    const before = digest(root);
    const automatic = run({ ...sync, invocation: 'automatic' });
    assert.equal(automatic.result, 'blocked');
    assert.equal(automatic.data.code, 'AUTOMATIC_MUTATION_BLOCKED');
    assert.equal(digest(root), before);
  });

  test('bounded primary effects create, format, relate, and machine-verify through the writer', (t) => {
    const root = bundle(t);
    const create = request(root, 'okf-write', 'create', { concept: 'new.md', set: { type: 'Note' } });
    create.scope = { concepts: ['new.md'] };
    assert.equal(run(create).result, 'applied');
    assert.match(fs.readFileSync(path.join(root, 'new.md'), 'utf8'), /status: draft/);

    concept(root);
    fs.writeFileSync(path.join(root, 'source.md'), '---\ntype: Note\n---\n# Source\n');
    fs.writeFileSync(path.join(root, 'note.md'), '---\ntype: Note\ntitle: Before\n---\n# Note\n');
    assert.equal(run(request(root, 'okf-write', 'format', { set: {}, evidence: undefined })).result, 'applied');
    assert.equal(run(request(root, 'okf-write', 'relationship', { evidence: [binding(root, 'source.md')], set: { sources: [{ resource: 'source.md' }] } })).result, 'applied');
    assert.equal(run(request(root, 'okf-write', 'machine-verify', {
      set: { verified: [{ kind: 'machine', by: 'check', coverage: 'complete-current-concept' }] },
    })).result, 'applied');
  });

  test('no-op, abstained, and blocked bounded outcomes do not hide their state', (t) => {
    const root = bundle(t);
    concept(root);
    const revise = request(root, 'okf-write', 'revise');
    assert.equal(run(revise).result, 'applied');
    assert.equal(run(revise).result, 'no-op');

    const abstain = request(root, 'okf-lifecycle', 'sync');
    delete abstain.payload.set;
    const abstained = run(abstain);
    assert.equal(abstained.result, 'abstained');
    assert.equal(abstained.data.authorization, 'allowed');
    const noScope = request(root, 'okf-lifecycle', 'sync');
    delete noScope.scope;
    assert.equal(run(noScope).data.code, 'INVALID_SCOPE');

    if (process.getuid && process.getuid() === 0) {
      t.skip('root can write through a read-only directory');
    } else {
      fs.chmodSync(root, 0o555);
      try {
        const failed = request(root, 'okf-write', 'create', { concept: 'missing/note.md', set: { type: 'Note' } });
        failed.scope = { concepts: ['missing/note.md'] };
        const response = run(failed);
        assert.equal(response.result, 'blocked');
        assert.ok(response.findings.some((item) => item.code === 'PARENT_DIRECTORY_NOT_WRITABLE'));
      } finally {
        fs.chmodSync(root, 0o755);
      }
    }
  });

  test('mode, scope, ownership, evidence, and semantic gates block only the request', (t) => {
    const root = bundle(t);
    concept(root);
    // #197: `project_mode` is a required, non-empty-string manifest bundle-record
    // field now, not optional `index.md` frontmatter -- `manifest.validate()`
    // itself rejects an absent one as an invalid manifest (refused at the
    // activation gate, not this write gate), so "missing mode" has no equivalent
    // here. "conflicting mode" (a YAML frontmatter block repeating one key) has
    // none either -- `JSON.parse` silently keeps the last of two duplicate JSON
    // keys rather than refusing the document. Only a present-but-unrecognized
    // value stays expressible at this gate.
    const cases = [
      ['invalid mode', () => setManifest(root, { projectMode: 'other' }), 'PROJECT_MODE_INVALID'],
    ];
    for (const [label, setup, code] of cases) {
      setup();
      const response = run(request(root, 'okf-write', 'revise'));
      assert.equal(response.result, 'blocked', label);
      assert.equal(response.data.code, code, label);
      assert.equal(fs.readFileSync(path.join(root, 'note.md'), 'utf8').includes('After'), false, label);
      setManifest(root);
    }
    assert.equal(run(request(root, 'okf-write', 'revise', { evidence: ['evidence.md'] })).data.code, 'UNSUPPORTED_INPUT');
    const invalidScope = request(root, 'okf-write', 'revise');
    invalidScope.scope = { concepts: ['other.md'] };
    assert.equal(run(invalidScope).data.code, 'INVALID_SCOPE');
    assert.equal(run(request(root, 'okf-write', 'revise', { set: { huge: 1e21 } })).result, 'blocked');

    const outside = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'okf-53-outside-')));
    t.after(() => fs.rmSync(outside, { recursive: true, force: true }));
    fs.writeFileSync(path.join(outside, 'index.md'), '---\nokf_version: "0.2"\nproject_mode: "knowledge-only"\n---\n# Bundle\n');
    const ownership = request(root, 'okf-write', 'revise', { bundle: outside });
    assert.equal(run(ownership).data.code, 'WRITE_OWNERSHIP_UNKNOWN');
  });

  test('unknown operations differ from unsupported bounded effects and no state files are created', (t) => {
    const root = bundle(t, 'code-backed');
    concept(root);
    const unknown = run(request(root, 'okf-write', 'archive'));
    assert.equal(unknown.data.code, 'UNKNOWN_OPERATION');
    const recoverable = run(request(root, 'okf-write', 'revise', { code_recoverable: true }));
    assert.equal(recoverable.data.code, 'CODE_RECOVERABLE_MATERIAL');
    const unsupported = run(request(root, 'okf-write', 'revise', { set: { status: 'stable' } }));
    assert.equal(unsupported.data.code, 'UNSUPPORTED_INPUT');
    const stateFiles = [];
    (function visit(directory) {
      for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
        const file = path.join(directory, entry.name);
        if (entry.isDirectory()) visit(file);
        else if (['guard', 'ledger', 'manifest'].some((word) => entry.name.includes(word))) stateFiles.push(file);
      }
    })(root);
    assert.deepEqual(stateFiles, []);
  });
});

describe('root declaration and semantic-preservation gates', () => {
  const wrapper = path.join(scripts, 'okf-write.js');

  // #197: `okf_version`/`project_mode` are the selected manifest bundle record's
  // fields now, not the bundle root's own `index.md` frontmatter -- the root is
  // navigation only. `bundle()` varies the manifest, never the root file.
  function bundle(t, { okfVersion = '0.2', projectMode = 'knowledge-only' } = {}) {
    const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'okf-write-gate-')));
    t.after(() => fs.rmSync(root, { recursive: true, force: true }));
    fs.mkdirSync(path.join(root, '.git'));
    fs.writeFileSync(path.join(root, '.okf-workspace.json'), JSON.stringify({
      schema_version: 1,
      workspace_id: '3f8c1b2e-4a5d-4e6f-8a9b-0c1d2e3f4a5b',
      repositories: [{ name: 'repo', path: '.', local: true }],
      bundles: [{ alias: 'repo', owner: 'repo', root: '.', okf_version: okfVersion, project_mode: projectMode }],
    }));
    fs.writeFileSync(path.join(root, 'index.md'), '# Bundle\n');
    fs.writeFileSync(path.join(root, 'evidence.md'), 'observed evidence\n');
    fs.writeFileSync(path.join(root, 'note.md'), '---\ntype: Note\ntitle: Before\n---\n# Note\n');
    return root;
  }

  function request(root, concept = 'note.md', set = { title: 'After' }) {
    return {
      protocol: 'okf-wrapper/1',
      skill: 'okf-write',
      operation: 'revise',
      task_kind: 'fix',
      scope: { concepts: [concept] },
      payload: { cwd: root, bundle: root, concept, set, evidence: [binding(root, 'evidence.md')] },
    };
  }

  function run(value) {
    const result = cp.spawnSync(process.execPath, [wrapper], {
      input: JSON.stringify(value), encoding: 'utf8',
    });
    const response = JSON.parse(result.stdout);
    assert.equal(result.status, 0);
    assert.equal(result.stderr, '');
    assert.equal(result.stdout, `${JSON.stringify(response)}\n`);
    return response;
  }

  function refusingVersion(t, okfVersion, observed, observedType) {
    const root = bundle(t, { okfVersion });
    const response = run(request(root));
    const finding = response.findings.find((item) => item.code === 'ROOT_DECLARATION_NOT_EXACT');
    assert.equal(response.result, 'blocked');
    assert.ok(finding, 'ROOT_DECLARATION_NOT_EXACT');
    assert.equal(finding.origin, 'suite');
    assert.deepEqual(finding.detail, { observed, observed_type: observedType });
  }

  // "An undeclared bundle root" (no `okf_version` at all) has no equivalent here
  // (#197): `okf_version` is a required, non-empty-string field of every manifest
  // bundle record (`manifest.js`'s `validate()`), so a manifest that omits it is
  // itself invalid and never reaches an admitted candidate at all -- the request
  // is refused as invalid configuration before `checkRoot` ever runs. Only a
  // present-but-wrong value stays expressible, covered below.

  test('write gate refuses a future-version bundle root', (t) => {
    refusingVersion(t, '0.3', '0.3', 'string');
  });

  test('write gate refuses a legacy bundle root', (t) => {
    refusingVersion(t, '0.1', '0.1', 'string');
  });

  test('write gate accepts a conforming bundle root and saves the revision', (t) => {
    const root = bundle(t);
    const response = run(request(root));
    assert.equal(response.result, 'applied');
    assert.deepEqual(fs.readFileSync(path.join(root, 'note.md')), Buffer.from('---\ntitle: After\ntype: Note\n---\n# Note\n'));
  });

  test('semantic-preservation mismatch blocks only the affected write before it reaches disk', (t) => {
    const root = bundle(t);
    const file = path.join(root, 'note.md');
    const before = fs.readFileSync(file);
    const response = run(request(root, 'note.md', { revision: 1e21 }));
    const finding = response.findings.find((item) => item.code === 'PARSE_TREE_MISMATCH');
    assert.equal(response.result, 'blocked');
    assert.ok(finding, 'PARSE_TREE_MISMATCH');
    assert.equal(finding.detail.construct, 'revision');
    assert.deepEqual(fs.readFileSync(file), before);

    const derivative = path.join(root, 'derivative.md');
    fs.writeFileSync(derivative, '---\ntype: Note\nsources:\n  - resource: note.md\n---\n# Derivative\n');
    const derivativeResponse = run(request(root, 'derivative.md'));
    assert.equal(derivativeResponse.result, 'applied');
    assert.deepEqual(fs.readFileSync(derivative), Buffer.from('---\nsources:\n  - resource: note.md\ntitle: After\ntype: Note\n---\n# Derivative\n'));
  });

  test('a derivative of a blocked concept is blocked before it reaches disk', (t) => {
    const root = bundle(t);
    fs.writeFileSync(path.join(root, 'source.md'), '---\ntitle: Blocked source\n---\n# Source\n');
    const derivative = path.join(root, 'derivative.md');
    fs.writeFileSync(derivative, '---\ntype: Note\nsources:\n  - resource: source.md\n---\n# Derivative\n');
    const before = fs.readFileSync(derivative);
    const response = run(request(root, 'derivative.md'));
    const finding = response.findings.find((item) => item.code === 'DEPENDS_ON_BLOCKED_CONCEPT');
    assert.equal(response.result, 'blocked');
    assert.ok(finding, 'DEPENDS_ON_BLOCKED_CONCEPT');
    assert.deepEqual(finding.detail, { path: 'derivative.md', blocked_concept: 'source.md' });
    assert.deepEqual(fs.readFileSync(derivative), before);
  });
});
