import { describe, expect, test } from 'vitest';

import {
  computeBindingKey,
  computeDesiredSpecHash,
  computeNativeDbApplicationToken,
  computeNativeDbBindingToken,
  computeNativeDbDatabaseName,
  computeRequirementKey,
  containsCredentialMaterial,
  isSecretKey,
  isTerminalLifecycleState,
  nativeDbIamSessionPolicy,
  parseNativeDbIamDescriptor,
  redactLifecycleSecrets
} from './index.js';

describe('database lifecycle contracts', () => {
  test('computes stable requirement, binding, and desired spec identifiers', async () => {
    const requirement = {
      logicalName: 'Orders DB',
      engine: 'postgres',
      version: '16',
      sizing: {
        storageGb: 20,
        tier: 'shared'
      },
      extensions: ['pgcrypto', 'uuid-ossp']
    } as const;

    expect(computeRequirementKey(requirement)).toBe('orders-db~Orders%20DB:postgres');
    expect(computeRequirementKey({ logicalName: 'orders/db', engine: 'postgres' })).toBe('orders-db~orders%2Fdb:postgres');
    expect(computeRequirementKey({ logicalName: 'orders-db', engine: 'postgres' })).toBe('orders-db~orders-db:postgres');
    // binding_key excludes the logical name on purpose: one database per
    // (organization, application, environment, profile). Two different logical
    // names under the same profile resolve to the same binding_key.
    expect(
      computeBindingKey({
        organizationId: 'org-1',
        applicationId: 'app-1',
        environment: 'edit',
        profile: 'Staging EU'
      })
    ).toBe('org-1:app-1:edit:staging-eu~Staging%20EU');
    expect(
      computeBindingKey({
        organizationId: 'org-1',
        applicationId: 'app-1',
        environment: 'edit',
        profile: 'staging'
      })
    ).toBe('org-1:app-1:edit:staging~staging');

    await expect(computeDesiredSpecHash(requirement)).resolves.toBe(
      await computeDesiredSpecHash({ ...requirement, extensions: ['uuid-ossp', 'pgcrypto'] })
    );
    await expect(computeDesiredSpecHash(requirement)).resolves.toBe('QsCl6RVzJVEwf4Nuk0D27rRtycYe2vQZMxcXLt9WUkQ=');
  });

  test('ignores migrationDirectory when hashing the desired spec', async () => {
    const infrastructureSpec = {
      logicalName: 'Orders DB',
      engine: 'postgres',
      version: '16',
      sizing: { storageGb: 20, tier: 'shared' },
      extensions: ['pgcrypto', 'uuid-ossp']
    } as const;

    const withoutDirectory = await computeDesiredSpecHash(infrastructureSpec);
    const withDirectory = await computeDesiredSpecHash({ ...infrastructureSpec, migrationDirectory: 'db/migrations' });
    const withOtherDirectory = await computeDesiredSpecHash({ ...infrastructureSpec, migrationDirectory: 'db/other-migrations' });

    // migrationDirectory only says where the SQL files live, not what
    // infrastructure to provision, so changing it must not look like drift.
    expect(withDirectory).toBe(withoutDirectory);
    expect(withOtherDirectory).toBe(withoutDirectory);
  });

  test('hashes desired specs with locale-invariant ordering', async () => {
    const first = await computeDesiredSpecHash({
      logicalName: 'Éclair DB',
      engine: 'postgres',
      extensions: ['ä', 'z', 'a']
    });
    const second = await computeDesiredSpecHash({
      logicalName: 'Éclair DB',
      engine: 'postgres',
      extensions: ['z', 'a', 'ä']
    });

    expect(first).toBe(second);
  });

  test('redacts raw credential material while preserving credential refs', () => {
    const redacted = redactLifecycleSecrets({
      host: 'db.internal',
      password: 'raw-password',
      nested: {
        token: 'raw-token',
        credentialRef: {
          resolver: 'aws_secrets_manager',
          ref: 'arn:aws:secretsmanager:us-east-1:123:secret:orders'
        }
      },
      credential_refs: {
        password: {
          resolver: 'vault',
          ref: 'database/orders',
          field: 'password'
        }
      }
    });

    expect(redacted).toEqual({
      host: 'db.internal',
      password: '[REDACTED]',
      nested: {
        token: '[REDACTED]',
        credentialRef: {
          resolver: 'aws_secrets_manager',
          ref: 'arn:aws:secretsmanager:us-east-1:123:secret:orders'
        }
      },
      credential_refs: {
        password: {
          resolver: 'vault',
          ref: 'database/orders',
          field: 'password'
        }
      }
    });
  });

  test('recognizes terminal lifecycle states', () => {
    expect(isTerminalLifecycleState('ready')).toBe(true);
    expect(isTerminalLifecycleState('failed')).toBe(true);
    expect(isTerminalLifecycleState('provisioning')).toBe(false);
  });

  test('flags secret-shaped keys', () => {
    for (const key of ['password', 'PASSWORD', 'api_token', 'private_key', 'private-key', 'dsn']) {
      expect(isSecretKey(key)).toBe(true);
    }
    for (const key of ['host', 'port', 'database', 'username']) {
      expect(isSecretKey(key)).toBe(false);
    }
  });

  test('flags secret-shaped value content', () => {
    expect(containsCredentialMaterial('postgres://user:pass@db.internal:5432/orders')).toBe(true);
    expect(containsCredentialMaterial('mongodb+srv://app:secret@cluster0.example.net/db')).toBe(true);
    expect(containsCredentialMaterial('password=hunter2;host=db.internal')).toBe(true);
    expect(containsCredentialMaterial('{"password":"hunter2"}')).toBe(true);
    expect(containsCredentialMaterial('host=db.internal port=5432')).toBe(false);
    expect(containsCredentialMaterial('arn:aws:secretsmanager:us-east-1:123:secret:orders')).toBe(false);
  });

  test('builds a canonical session policy from a valid managed Postgres IAM descriptor', () => {
    const descriptor = parseNativeDbIamDescriptor({
      application_id: 'application-id',
      auth_descriptor_version: 1,
      auth_mode: 'aws_iam_role',
      aws_account_id: '123456789012',
      binding_id: 'binding-id',
      cluster_resource_id: 'cluster-ABC123DEF456EXAMPLE',
      connector_role_arn: 'arn:aws:iam::123456789012:role/superblocks-native-db-connector-prod',
      database: 'sbndb_abcdef123456_0123456789abcdef01234567',
      host: 'native-db.cluster-example.us-west-2.rds.amazonaws.com',
      port: 5432,
      region: 'us-west-2',
      username: 'sbndb_abcdef123456_7bfeae1170db923274f1943f_runtime'
    });

    expect(descriptor).toBeDefined();
    if (!descriptor) {
      throw new Error('Expected a valid IAM descriptor');
    }
    expect(nativeDbIamSessionPolicy(descriptor)).toBe(
      '{"Version":"2012-10-17","Statement":[{"Sid":"ConnectToThisNativeDatabaseUser","Effect":"Allow","Action":"rds-db:connect","Resource":"arn:aws:rds-db:us-west-2:123456789012:dbuser:cluster-ABC123DEF456EXAMPLE/sbndb_abcdef123456_7bfeae1170db923274f1943f_runtime"}]}'
    );
  });

  test('reads binding-scoped v2 managed Postgres IAM descriptors', () => {
    expect(
      parseNativeDbIamDescriptor({
        application_id: 'application-id',
        auth_descriptor_version: 2,
        auth_mode: 'aws_iam_role',
        aws_account_id: '123456789012',
        binding_id: 'binding-id',
        cluster_resource_id: 'cluster-ABC123DEF456EXAMPLE',
        connector_role_arn: 'arn:aws:iam::123456789012:role/superblocks-native-db-connector-prod',
        database: 'sbndb_abcdef123456_0123456789abcdef01234567',
        host: 'native-db.cluster-example.us-west-2.rds.amazonaws.com',
        port: 5432,
        region: 'us-west-2',
        username: 'sbndb_abcdef123456_0123456789abcdef01234567_runtime'
      })
    ).toEqual({
      application_id: 'application-id',
      auth_descriptor_version: 2,
      auth_mode: 'aws_iam_role',
      aws_account_id: '123456789012',
      binding_id: 'binding-id',
      cluster_resource_id: 'cluster-ABC123DEF456EXAMPLE',
      connector_role_arn: 'arn:aws:iam::123456789012:role/superblocks-native-db-connector-prod',
      database: 'sbndb_abcdef123456_0123456789abcdef01234567',
      host: 'native-db.cluster-example.us-west-2.rds.amazonaws.com',
      port: 5432,
      region: 'us-west-2',
      username: 'sbndb_abcdef123456_0123456789abcdef01234567_runtime'
    });
  });

  test('rejects v2 descriptors when database and runtime username use different binding tokens', () => {
    expect(
      parseNativeDbIamDescriptor({
        application_id: 'application-id',
        auth_descriptor_version: 2,
        auth_mode: 'aws_iam_role',
        aws_account_id: '123456789012',
        binding_id: 'binding-id',
        cluster_resource_id: 'cluster-ABC123DEF456EXAMPLE',
        connector_role_arn: 'arn:aws:iam::123456789012:role/superblocks-native-db-connector-prod',
        database: 'sbndb_abcdef123456_0123456789abcdef01234567',
        host: 'native-db.cluster-example.us-west-2.rds.amazonaws.com',
        port: 5432,
        region: 'us-west-2',
        username: 'sbndb_abcdef123456_fedcba9876543210fedcba98_runtime'
      })
    ).toBeUndefined();
  });

  test('rejects malformed or non-commercial managed Postgres IAM descriptors', () => {
    const descriptor = {
      application_id: 'application-id',
      auth_descriptor_version: 1,
      auth_mode: 'aws_iam_role',
      aws_account_id: '123456789012',
      binding_id: 'binding-id',
      cluster_resource_id: 'cluster-ABC123DEF456EXAMPLE',
      connector_role_arn: 'arn:aws:iam::123456789012:role/superblocks-native-db-connector-prod',
      database: 'sbndb_abcdef123456_0123456789abcdef01234567',
      host: 'native-db.cluster-example.us-west-2.rds.amazonaws.com',
      port: 5432,
      region: 'us-west-2',
      username: 'sbndb_abcdef123456_7bfeae1170db923274f1943f_runtime'
    };

    expect(parseNativeDbIamDescriptor({ ...descriptor, auth_descriptor_version: 3 })).toBeUndefined();
    expect(parseNativeDbIamDescriptor({ ...descriptor, cluster_resource_id: 'not-an-rds-resource' })).toBeUndefined();
    expect(parseNativeDbIamDescriptor({ ...descriptor, connector_role_arn: 'arn:aws:iam::210987654321:role/other' })).toBeUndefined();
    expect(parseNativeDbIamDescriptor({ ...descriptor, database: 'orders' })).toBeUndefined();
    expect(parseNativeDbIamDescriptor({ ...descriptor, host: descriptor.host.toUpperCase() })).toBeUndefined();
    expect(parseNativeDbIamDescriptor({ ...descriptor, host: 'native-db.cluster-example.us-east-1.rds.amazonaws.com' })).toBeUndefined();
    expect(parseNativeDbIamDescriptor({ ...descriptor, port: 0 })).toBeUndefined();
    expect(parseNativeDbIamDescriptor({ ...descriptor, region: 'cn-north-1' })).toBeUndefined();
    expect(parseNativeDbIamDescriptor({ ...descriptor, region: 'us-gov-west-1' })).toBeUndefined();
    expect(parseNativeDbIamDescriptor({ ...descriptor, username: 'sbndb_000000000000_7bfeae1170db923274f1943f_runtime' })).toBeUndefined();
  });

  test('accepts standalone RDS IAM resource IDs', () => {
    expect(
      parseNativeDbIamDescriptor({
        application_id: 'application-id',
        auth_descriptor_version: 1,
        auth_mode: 'aws_iam_role',
        aws_account_id: '123456789012',
        binding_id: 'binding-id',
        cluster_resource_id: 'db-ABCDEF0123456789-1',
        connector_role_arn: 'arn:aws:iam::123456789012:role/superblocks-native-db-connector-prod',
        database: 'sbndb_abcdef123456_0123456789abcdef01234567',
        host: 'native-db.example.us-west-2.rds.amazonaws.com',
        port: 5432,
        region: 'us-west-2',
        username: 'sbndb_abcdef123456_7bfeae1170db923274f1943f_runtime'
      })
    ).toBeDefined();
  });

  test('derives canonical IAM database and runtime-user identity tokens', async () => {
    await expect(computeNativeDbApplicationToken('application-id')).resolves.toBe('7bfeae1170db923274f1943f');
    await expect(computeNativeDbDatabaseName('abcdef123456', 'database-id')).resolves.toBe('sbndb_abcdef123456_dad72612f7a80580b028178f');
  });

  test('derives a stable binding identity token for v2 database and runtime-user names', async () => {
    await expect(computeNativeDbBindingToken('binding-id')).resolves.toBe('927968fc37b7b41be7e14cf9');
  });
});
