import { TestBed } from '@angular/core/testing';
import { firstValueFrom } from 'rxjs';
import { PartnerHierarchyLocalGateway } from './partner-hierarchy-local.gateway';
import { ProvisioningNode } from '../models/provisioning-node.model';

/**
 * These assert the §2 invariants from
 * docs/backend/PARTNER-PROVISIONING-API-CONTRACT.md, not just that the mock
 * returns something. A mock that accepts what the real backend rejects would
 * let the UI ship a flow the backend then refuses.
 */
describe('PartnerHierarchyLocalGateway (contract invariants)', () => {
  let gateway: PartnerHierarchyLocalGateway;

  beforeEach(() => {
    TestBed.configureTestingModule({});
    gateway = TestBed.inject(PartnerHierarchyLocalGateway);
  });

  it('seeds a full four-level hierarchy', async () => {
    const nodes = await firstValueFrom(gateway.loadHierarchy('cmp_1', 'TEST'));

    expect([...nodes].map(n => String(n.level)).sort()).toEqual(
      ['company', 'payment_point', 'payment_point', 'project', 'store'],
    );
  });

  it('computes path server-side, never trusting a client-supplied one', async () => {
    const created = await firstValueFrom(
      gateway.createStore('prj_1', { externalReference: 'new-store', displayName: 'New Store' }),
    );

    expect(created.path).toEqual(['cmp_1', 'prj_1', created.id]);
    expect(created.path[created.path.length - 1]).toBe(created.id);
  });

  it('inherits companyId and environment from the parent', async () => {
    const created = await firstValueFrom(
      gateway.createPaymentPoint('str_1', { externalReference: 'new-pp', displayName: 'New PP' }),
    );

    expect(created.companyId).toBe('cmp_1');
    expect(created.environment).toBe('TEST');
  });

  it('rejects a duplicate externalReference at the same level', async () => {
    await firstValueFrom(gateway.createStore('prj_1', { externalReference: 'dup', displayName: 'A' }));

    await expectAsync(
      firstValueFrom(gateway.createStore('prj_1', { externalReference: 'dup', displayName: 'B' })),
    ).toBeRejectedWithError(/already in use/);
  });

  it('allows the same externalReference at a different level', async () => {
    await firstValueFrom(gateway.createStore('prj_1', { externalReference: 'shared', displayName: 'Store' }));

    // Uniqueness is scoped per (companyId, environment, level) - a payment
    // point may reuse a store's reference without collision.
    const created = await firstValueFrom(
      gateway.createPaymentPoint('str_1', { externalReference: 'shared', displayName: 'PP' }),
    );

    expect(created.externalReference).toBe('shared');
  });

  it('rejects creating under a parent that does not exist', async () => {
    await expectAsync(
      firstValueFrom(gateway.createStore('nope', { externalReference: 'x', displayName: 'X' })),
    ).toBeRejectedWithError(/Parent not found/);
  });

  it('cascades disable to every descendant', async () => {
    await firstValueFrom(gateway.disable('str_1'));
    const nodes = await firstValueFrom(gateway.loadHierarchy('cmp_1', 'TEST'));

    const store = find(nodes, 'str_1');
    const paymentPoints = nodes.filter(n => n.level === 'payment_point');

    expect(store.status).toBe('disabled');
    expect(paymentPoints.every(n => n.status === 'disabled')).toBeTrue();
  });

  it('leaves nodes outside the disabled subtree untouched', async () => {
    await firstValueFrom(gateway.disable('str_1'));
    const nodes = await firstValueFrom(gateway.loadHierarchy('cmp_1', 'TEST'));

    expect(find(nodes, 'cmp_1').status).toBe('active');
    expect(find(nodes, 'prj_1').status).toBe('active');
  });

  it('treats disabled as terminal - no status change afterwards', async () => {
    await firstValueFrom(gateway.disable('str_1'));

    await expectAsync(firstValueFrom(gateway.setStatus('str_1', 'active'))).toBeRejectedWithError(
      /disabled node cannot change status/,
    );
  });

  it('allows suspend then reactivate', async () => {
    await firstValueFrom(gateway.setStatus('str_1', 'suspended'));
    const reactivated = await firstValueFrom(gateway.setStatus('str_1', 'active'));

    expect(reactivated.status).toBe('active');
  });

  it('returns null rather than a fuzzy match for an unknown externalReference', async () => {
    const found = await firstValueFrom(gateway.lookupByExternalReference('does-not-exist', 'TEST'));

    expect(found).toBeNull();
  });

  it('scopes lookup by environment', async () => {
    // Seed data is TEST-only; the same reference must not resolve under LIVE.
    const found = await firstValueFrom(gateway.lookupByExternalReference('demo-store-1', 'LIVE'));

    expect(found).toBeNull();
  });

  it('never returns a revoked credential to rotation', async () => {
    const credential = await firstValueFrom(
      gateway.registerCredential({
        scopeNodeId: 'prj_1',
        environment: 'TEST',
        algorithm: 'ed25519',
        publicKey: 'ssh-ed25519 AAAA...',
      }),
    );
    await firstValueFrom(gateway.revokeCredential(credential.keyId));

    await expectAsync(
      firstValueFrom(gateway.rotateCredential(credential.keyId, 'ssh-ed25519 BBBB...')),
    ).toBeRejectedWithError(/revoked credential cannot be rotated/);
  });

  it('scopes credentials to the requested company', async () => {
    await firstValueFrom(
      gateway.registerCredential({
        scopeNodeId: 'prj_1',
        environment: 'TEST',
        algorithm: 'ed25519',
        publicKey: 'ssh-ed25519 AAAA...',
      }),
    );

    const forCompany = await firstValueFrom(gateway.loadCredentials('cmp_1'));
    const forOther = await firstValueFrom(gateway.loadCredentials('cmp_other'));

    expect(forCompany.length).toBe(1);
    expect(forOther.length).toBe(0);
  });
});

function find(nodes: ProvisioningNode[], id: string): ProvisioningNode {
  const found = nodes.find(n => n.id === id);
  if (!found) {
    throw new Error(`expected node ${id} in hierarchy`);
  }
  return found;
}
