import { test, expect, describe } from 'bun:test';
import {
  hasScope,
  isScope,
  ALLOWED_SCOPES,
  ALLOWED_SCOPES_LIST,
  assertAllowedScopes,
  InvalidScopeError,
  parseScopeString,
  type Scope,
} from '../src/core/scope.ts';

// ---------------------------------------------------------------------------
// Hierarchy table — admin → all, write → read, sibling non-implication
// ---------------------------------------------------------------------------

describe('hasScope — admin implies all (escape hatch)', () => {
  const all: Scope[] = ['read', 'write', 'admin', 'sources_admin', 'users_admin'];
  for (const required of all) {
    test(`admin → ${required}`, () => {
      expect(hasScope(['admin'], required)).toBe(true);
    });
  }
});

describe('hasScope — write implies read but not admin variants', () => {
  test('write → read', () => {
    expect(hasScope(['write'], 'read')).toBe(true);
  });
  test('write → write', () => {
    expect(hasScope(['write'], 'write')).toBe(true);
  });
  test('write does NOT imply admin', () => {
    expect(hasScope(['write'], 'admin')).toBe(false);
  });
  test('write does NOT imply sources_admin', () => {
    expect(hasScope(['write'], 'sources_admin')).toBe(false);
  });
  test('write does NOT imply users_admin', () => {
    expect(hasScope(['write'], 'users_admin')).toBe(false);
  });
});

describe('hasScope — sibling non-implication for *_admin scopes', () => {
  test('sources_admin → sources_admin only', () => {
    expect(hasScope(['sources_admin'], 'sources_admin')).toBe(true);
    expect(hasScope(['sources_admin'], 'users_admin')).toBe(false);
    expect(hasScope(['sources_admin'], 'write')).toBe(false);
    expect(hasScope(['sources_admin'], 'read')).toBe(false);
    expect(hasScope(['sources_admin'], 'admin')).toBe(false);
  });
  test('users_admin → users_admin only', () => {
    expect(hasScope(['users_admin'], 'users_admin')).toBe(true);
    expect(hasScope(['users_admin'], 'sources_admin')).toBe(false);
    expect(hasScope(['users_admin'], 'write')).toBe(false);
    expect(hasScope(['users_admin'], 'read')).toBe(false);
  });
});

describe('hasScope — read scope', () => {
  test('read → read', () => {
    expect(hasScope(['read'], 'read')).toBe(true);
  });
  test('read does NOT imply write', () => {
    expect(hasScope(['read'], 'write')).toBe(false);
  });
});

describe('hasScope — empty + unknown granted', () => {
  test('empty granted set returns false', () => {
    expect(hasScope([], 'read')).toBe(false);
  });
  test('unknown scope strings ignored gracefully (forward-compat)', () => {
    expect(hasScope(['flying-unicorn'], 'read')).toBe(false);
    expect(hasScope(['flying-unicorn', 'admin'], 'read')).toBe(true); // admin still implies
  });
});

describe('hasScope — multi-grant', () => {
  test('read + sources_admin combo', () => {
    expect(hasScope(['read', 'sources_admin'], 'read')).toBe(true);
    expect(hasScope(['read', 'sources_admin'], 'sources_admin')).toBe(true);
    expect(hasScope(['read', 'sources_admin'], 'write')).toBe(false);
  });
  test('write + sources_admin combo (gstack /setup-gbrain Path 4 token)', () => {
    expect(hasScope(['write', 'sources_admin'], 'read')).toBe(true);
    expect(hasScope(['write', 'sources_admin'], 'write')).toBe(true);
    expect(hasScope(['write', 'sources_admin'], 'sources_admin')).toBe(true);
    expect(hasScope(['write', 'sources_admin'], 'users_admin')).toBe(false);
  });
});

// ---------------------------------------------------------------------------
// F3 invariant: refresh-token requested-subset enforcement uses hasScope.
// Prove the v0.26.9-correct semantics (admin grant CAN refresh down to a
// subset; non-implied scope refresh fails).
// ---------------------------------------------------------------------------

describe('F3 refresh-token subset semantics under hasScope', () => {
  test('admin grant → refresh requesting sources_admin succeeds', () => {
    const granted = ['admin'];
    const requested = ['sources_admin'];
    expect(requested.every(s => hasScope(granted, s))).toBe(true);
  });
  test('admin grant → refresh requesting subset (read+write) succeeds', () => {
    const granted = ['admin'];
    const requested = ['read', 'write'];
    expect(requested.every(s => hasScope(granted, s))).toBe(true);
  });
  test('write grant → refresh requesting admin fails', () => {
    const granted = ['write'];
    const requested = ['admin'];
    expect(requested.every(s => hasScope(granted, s))).toBe(false);
  });
  test('write grant → refresh requesting sources_admin fails', () => {
    const granted = ['write'];
    const requested = ['sources_admin'];
    expect(requested.every(s => hasScope(granted, s))).toBe(false);
  });
  test('sources_admin grant → refresh requesting users_admin fails (sibling axis)', () => {
    const granted = ['sources_admin'];
    const requested = ['users_admin'];
    expect(requested.every(s => hasScope(granted, s))).toBe(false);
  });
});

// ---------------------------------------------------------------------------
// ALLOWED_SCOPES allowlist (D4) — registration-time gate
// ---------------------------------------------------------------------------

describe('ALLOWED_SCOPES — exact list pinned', () => {
  test('contains the 6 canonical scopes (v0.38: agent added)', () => {
    expect(ALLOWED_SCOPES.size).toBe(6);
    expect(ALLOWED_SCOPES.has('read')).toBe(true);
    expect(ALLOWED_SCOPES.has('write')).toBe(true);
    expect(ALLOWED_SCOPES.has('admin')).toBe(true);
    expect(ALLOWED_SCOPES.has('sources_admin')).toBe(true);
    expect(ALLOWED_SCOPES.has('users_admin')).toBe(true);
    expect(ALLOWED_SCOPES.has('agent')).toBe(true);
  });
  test('list is sorted alphabetically (deterministic for wire/drift check)', () => {
    expect([...ALLOWED_SCOPES_LIST]).toEqual([
      'admin',
      'agent',
      'read',
      'sources_admin',
      'users_admin',
      'write',
    ]);
  });
});

describe('isScope', () => {
  test('accepts allowed strings', () => {
    expect(isScope('read')).toBe(true);
    expect(isScope('sources_admin')).toBe(true);
  });
  test('rejects unknown strings', () => {
    expect(isScope('flying-unicorn')).toBe(false);
    expect(isScope('')).toBe(false);
    expect(isScope('READ')).toBe(false); // case-sensitive
  });
});

describe('assertAllowedScopes', () => {
  test('passes for valid set', () => {
    expect(() => assertAllowedScopes(['read', 'sources_admin'])).not.toThrow();
  });
  test('passes for empty', () => {
    expect(() => assertAllowedScopes([])).not.toThrow();
  });
  test('throws InvalidScopeError naming the bad scope', () => {
    try {
      assertAllowedScopes(['read', 'flying-unicorn']);
      throw new Error('expected throw');
    } catch (e) {
      expect(e).toBeInstanceOf(InvalidScopeError);
      expect((e as InvalidScopeError).invalidScope).toBe('flying-unicorn');
      expect((e as InvalidScopeError).message).toMatch(/flying-unicorn/);
      expect((e as InvalidScopeError).message).toMatch(/Allowed:/);
    }
  });
  test('throws on first invalid scope (short-circuits)', () => {
    try {
      assertAllowedScopes(['flying-unicorn', 'also-bad']);
      throw new Error('expected throw');
    } catch (e) {
      expect((e as InvalidScopeError).invalidScope).toBe('flying-unicorn');
    }
  });
});

describe('parseScopeString', () => {
  test('splits space-separated', () => {
    expect(parseScopeString('read write admin')).toEqual(['read', 'write', 'admin']);
  });
  test('drops empty fragments', () => {
    expect(parseScopeString('read  write')).toEqual(['read', 'write']);
  });
  test('handles undefined/null/empty', () => {
    expect(parseScopeString(undefined)).toEqual([]);
    expect(parseScopeString(null)).toEqual([]);
    expect(parseScopeString('')).toEqual([]);
  });
  test('does NOT validate (separation of concerns)', () => {
    expect(parseScopeString('read flying-unicorn')).toEqual(['read', 'flying-unicorn']);
  });
});
