/**
 * Local organizations: org CRUD + membership + the session-orgIds query that
 * makes the existing provider-agnostic 'org' visibility work for local users.
 *
 * See docs/superpowers/plans/2026-06-09-local-orgs.md.
 */
import { afterEach, beforeEach, describe, it, expect } from 'vitest';
import { mkdtempSync, rmSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { Repository } from './repository.js';
import { runMigrations } from './migrate.js';

describe('Repository local orgs', () => {
  let tempDir = '';
  let repo: Repository;
  let alice: string;
  let bob: string;

  beforeEach(() => {
    tempDir = mkdtempSync(join(tmpdir(), 'maestro-localorgs-'));
    repo = new Repository(join(tempDir, 'orchestrator.db'));
    runMigrations(repo.getDb());
    alice = repo.createUser({ email: 'alice@x.com', name: 'Alice', role: 'user', status: 'active' }).id;
    bob = repo.createUser({ email: 'bob@x.com', name: 'Bob', role: 'user', status: 'active' }).id;
  });
  afterEach(() => {
    repo.close();
    if (tempDir) { rmSync(tempDir, { recursive: true, force: true }); tempDir = ''; }
  });

  it('createLocalOrg assigns an lorg: id and is listable', () => {
    const org = repo.createLocalOrg('Team A', alice);
    expect(org.id).toMatch(/^lorg:/);
    expect(org.name).toBe('Team A');
    expect(org.createdBy).toBe(alice);
    expect(repo.listLocalOrgs().map(o => o.id)).toContain(org.id);
    expect(repo.getLocalOrg(org.id)?.name).toBe('Team A');
  });

  it('lorg ids do not collide with Gitea numeric org ids', () => {
    const org = repo.createLocalOrg('Team', alice);
    // A Gitea org id like '42' must never equal a local org id.
    expect(org.id).not.toBe('42');
    expect(org.id.startsWith('lorg:')).toBe(true);
  });

  it('renameLocalOrg updates the name', () => {
    const org = repo.createLocalOrg('Old', alice);
    repo.renameLocalOrg(org.id, 'New');
    expect(repo.getLocalOrg(org.id)?.name).toBe('New');
  });

  it('membership add/remove + listOrgMembers', () => {
    const org = repo.createLocalOrg('Team', alice);
    repo.addOrgMember(org.id, alice);
    repo.addOrgMember(org.id, bob, 'owner');
    const members = repo.listOrgMembers(org.id);
    expect(members.map(m => m.userId).sort()).toEqual([alice, bob].sort());
    expect(members.find(m => m.userId === bob)?.role).toBe('owner');
    repo.removeOrgMember(org.id, bob);
    expect(repo.listOrgMembers(org.id).map(m => m.userId)).toEqual([alice]);
  });

  it('addOrgMember is idempotent (re-add does not duplicate / can update role)', () => {
    const org = repo.createLocalOrg('Team', alice);
    repo.addOrgMember(org.id, alice, 'member');
    repo.addOrgMember(org.id, alice, 'owner');
    const members = repo.listOrgMembers(org.id);
    expect(members).toHaveLength(1);
    expect(members[0].role).toBe('owner');
  });

  it('listUserLocalOrgs returns the orgs a user belongs to (feeds session.orgIds)', () => {
    const a = repo.createLocalOrg('A', alice);
    const b = repo.createLocalOrg('B', alice);
    repo.createLocalOrg('C', alice); // alice not a member
    repo.addOrgMember(a.id, alice);
    repo.addOrgMember(b.id, alice);
    const orgs = repo.listUserLocalOrgs(alice);
    expect(orgs.map(o => o.orgId).sort()).toEqual([a.id, b.id].sort());
    expect(orgs.find(o => o.orgId === a.id)?.name).toBe('A');
    expect(repo.listUserLocalOrgs(bob)).toEqual([]);
  });

  it('deleteLocalOrg cascades members', () => {
    const org = repo.createLocalOrg('Team', alice);
    repo.addOrgMember(org.id, alice);
    repo.deleteLocalOrg(org.id);
    expect(repo.getLocalOrg(org.id)).toBeNull();
    expect(repo.listOrgMembers(org.id)).toEqual([]);
    expect(repo.listUserLocalOrgs(alice)).toEqual([]);
  });

  it('deleteLocalOrg downgrades org-scoped resources to private (no orphan)', () => {
    const org = repo.createLocalOrg('Team', alice);
    const db = repo.getDb();
    // A local_task scoped to this org (visibility='org').
    db.prepare(
      `INSERT INTO local_tasks (title, body, piece_name, profile, output_format, ask_policy, priority, workspace_path, owner_id, visibility, visibility_scope_org_id)
       VALUES ('t', 'b', 'general', 'auto', 'markdown', 'always', 'normal', '/tmp/x', @owner, 'org', @org)`,
    ).run({ owner: alice, org: org.id });
    const before = db.prepare("SELECT visibility, visibility_scope_org_id FROM local_tasks WHERE owner_id=?").get(alice) as { visibility: string; visibility_scope_org_id: string | null };
    expect(before.visibility).toBe('org');

    repo.deleteLocalOrg(org.id);

    const after = db.prepare("SELECT visibility, visibility_scope_org_id FROM local_tasks WHERE owner_id=?").get(alice) as { visibility: string; visibility_scope_org_id: string | null };
    expect(after.visibility).toBe('private'); // downgraded, still owner-visible
    expect(after.visibility_scope_org_id).toBeNull();
  });

  it('deleting a user cascades their memberships but leaves the org (created_by SET NULL)', () => {
    const org = repo.createLocalOrg('Team', alice);
    repo.addOrgMember(org.id, alice);
    repo.addOrgMember(org.id, bob);
    repo.deleteUser(bob);
    expect(repo.listOrgMembers(org.id).map(m => m.userId)).toEqual([alice]);
    // org still exists even if its creator is deleted
    repo.deleteUser(alice);
    expect(repo.getLocalOrg(org.id)).not.toBeNull();
    expect(repo.getLocalOrg(org.id)?.createdBy).toBeNull();
  });
});
