import { assertEquals, assertExists } from 'jsr:@std/assert';
import { afterAll, beforeAll, describe, it } from 'jsr:@std/testing/bdd';
import { TypedRequest } from '@api.global/typedrequest';
import { createTestContainer, getTestPorts, loginAndGetIdentity } from './helpers/server.helper.ts';
import { ObjectStorageContainer } from '../ts/index.ts';
import type * as interfaces from '../ts_interfaces/index.ts';
import type { IReq_CreateBucket, IReq_DeleteBucket, IReq_GetBucketPolicy } from '../ts_interfaces/requests/buckets.ts';
import type {
  IReq_ListNamedPolicies,
  IReq_CreateNamedPolicy,
  IReq_UpdateNamedPolicy,
  IReq_DeleteNamedPolicy,
  IReq_GetBucketNamedPolicies,
  IReq_AttachPolicyToBucket,
  IReq_DetachPolicyFromBucket,
  IReq_GetPolicyBuckets,
  IReq_SetPolicyBuckets,
} from '../ts_interfaces/requests/policies.ts';

const PORT_INDEX = 5;
const ports = getTestPorts(PORT_INDEX);
const url = `http://localhost:${ports.uiPort}/typedrequest`;

describe('Named policy management', { sanitizeResources: false, sanitizeOps: false }, () => {
  let container: ObjectStorageContainer;
  let identity: interfaces.data.IIdentity;
  let policy1Id: string;
  let policy2Id: string;

  beforeAll(async () => {
    container = createTestContainer(PORT_INDEX);
    await container.start();
    identity = await loginAndGetIdentity(ports.uiPort);

    // Create test buckets
    const createBucket = new TypedRequest<IReq_CreateBucket>(url, 'createBucket');
    await createBucket.fire({ identity, bucketName: 'pol-bucket-1' });
    await createBucket.fire({ identity, bucketName: 'pol-bucket-2' });
  });

  afterAll(async () => {
    // Cleanup
    try {
      const del = new TypedRequest<IReq_DeleteBucket>(url, 'deleteBucket');
      await del.fire({ identity, bucketName: 'pol-bucket-1' });
    } catch { /* may already be deleted */ }
    try {
      const del = new TypedRequest<IReq_DeleteBucket>(url, 'deleteBucket');
      await del.fire({ identity, bucketName: 'pol-bucket-2' });
    } catch { /* may already be deleted */ }
    await container.stop();
  });

  it('should list policies (initially empty)', async () => {
    const req = new TypedRequest<IReq_ListNamedPolicies>(url, 'listNamedPolicies');
    const response = await req.fire({ identity });
    assertEquals(response.policies.length, 0);
  });

  it('should create a named policy', async () => {
    const req = new TypedRequest<IReq_CreateNamedPolicy>(url, 'createNamedPolicy');
    const response = await req.fire({
      identity,
      name: 'Public Read',
      description: 'Allows public read access',
      statements: [
        {
          Sid: 'PublicRead',
          Effect: 'Allow',
          Principal: '*',
          Action: 's3:GetObject',
          Resource: 'arn:aws:s3:::${bucket}/*',
        },
      ],
    });
    assertExists(response.policy.id);
    assertEquals(response.policy.name, 'Public Read');
    assertEquals(response.policy.statements.length, 1);
    assertEquals(response.policy.createdAt > 0, true);
    policy1Id = response.policy.id;
  });

  it('should create a second policy', async () => {
    const req = new TypedRequest<IReq_CreateNamedPolicy>(url, 'createNamedPolicy');
    const response = await req.fire({
      identity,
      name: 'Deny Delete',
      description: 'Denies delete actions',
      statements: [
        {
          Sid: 'DenyDelete',
          Effect: 'Deny',
          Principal: '*',
          Action: 's3:DeleteObject',
          Resource: 'arn:aws:s3:::${bucket}/*',
        },
      ],
    });
    policy2Id = response.policy.id;
    assertExists(policy2Id);
  });

  it('should list both policies', async () => {
    const req = new TypedRequest<IReq_ListNamedPolicies>(url, 'listNamedPolicies');
    const response = await req.fire({ identity });
    assertEquals(response.policies.length, 2);
  });

  it('should get bucket named policies (none attached)', async () => {
    const req = new TypedRequest<IReq_GetBucketNamedPolicies>(url, 'getBucketNamedPolicies');
    const response = await req.fire({ identity, bucketName: 'pol-bucket-1' });
    assertEquals(response.attachedPolicies.length, 0);
    assertEquals(response.availablePolicies.length, 2);
  });

  it('should attach policy to bucket', async () => {
    const req = new TypedRequest<IReq_AttachPolicyToBucket>(url, 'attachPolicyToBucket');
    const response = await req.fire({ identity, policyId: policy1Id, bucketName: 'pol-bucket-1' });
    assertEquals(response.ok, true);
  });

  it('should verify bucket policy was applied with placeholder replaced', async () => {
    const req = new TypedRequest<IReq_GetBucketPolicy>(url, 'getBucketPolicy');
    const response = await req.fire({ identity, bucketName: 'pol-bucket-1' });
    assertExists(response.policy);
    const parsed = JSON.parse(response.policy!);
    const resource = parsed.Statement[0].Resource;
    // Resource may be a string or array depending on the S3 engine
    const resourceStr = Array.isArray(resource) ? resource.join(' ') : resource;
    // ${bucket} should be replaced with actual bucket name
    assertEquals(resourceStr.includes('pol-bucket-1'), true);
    assertEquals(resourceStr.includes('${bucket}'), false);
  });

  it('should get bucket named policies (one attached)', async () => {
    const req = new TypedRequest<IReq_GetBucketNamedPolicies>(url, 'getBucketNamedPolicies');
    const response = await req.fire({ identity, bucketName: 'pol-bucket-1' });
    assertEquals(response.attachedPolicies.length, 1);
    assertEquals(response.availablePolicies.length, 1);
    assertEquals(response.attachedPolicies[0].id, policy1Id);
  });

  it('should attach second policy to same bucket', async () => {
    const req = new TypedRequest<IReq_AttachPolicyToBucket>(url, 'attachPolicyToBucket');
    const response = await req.fire({ identity, policyId: policy2Id, bucketName: 'pol-bucket-1' });
    assertEquals(response.ok, true);
  });

  it('should verify merged policy has statements from both', async () => {
    const req = new TypedRequest<IReq_GetBucketPolicy>(url, 'getBucketPolicy');
    const response = await req.fire({ identity, bucketName: 'pol-bucket-1' });
    assertExists(response.policy);
    const parsed = JSON.parse(response.policy!);
    assertEquals(parsed.Statement.length >= 2, true);
    const sids = parsed.Statement.map((s: any) => s.Sid);
    assertEquals(sids.includes('PublicRead'), true);
    assertEquals(sids.includes('DenyDelete'), true);
  });

  it('should get policy buckets', async () => {
    const req = new TypedRequest<IReq_GetPolicyBuckets>(url, 'getPolicyBuckets');
    const response = await req.fire({ identity, policyId: policy1Id });
    assertEquals(response.attachedBuckets.includes('pol-bucket-1'), true);
    assertEquals(response.availableBuckets.includes('pol-bucket-2'), true);
  });

  it('should set policy buckets (batch)', async () => {
    const req = new TypedRequest<IReq_SetPolicyBuckets>(url, 'setPolicyBuckets');
    const response = await req.fire({
      identity,
      policyId: policy1Id,
      bucketNames: ['pol-bucket-1', 'pol-bucket-2'],
    });
    assertEquals(response.ok, true);
  });

  it('should verify policy applied to second bucket', async () => {
    const req = new TypedRequest<IReq_GetBucketPolicy>(url, 'getBucketPolicy');
    const response = await req.fire({ identity, bucketName: 'pol-bucket-2' });
    assertExists(response.policy);
    const parsed = JSON.parse(response.policy!);
    const resource = parsed.Statement[0].Resource;
    const resourceStr = Array.isArray(resource) ? resource.join(' ') : resource;
    assertEquals(resourceStr.includes('pol-bucket-2'), true);
  });

  it('should update a policy', async () => {
    const req = new TypedRequest<IReq_UpdateNamedPolicy>(url, 'updateNamedPolicy');
    const response = await req.fire({
      identity,
      policyId: policy1Id,
      name: 'Public Read Updated',
      description: 'Updated policy',
      statements: [
        {
          Sid: 'PublicReadUpdated',
          Effect: 'Allow',
          Principal: '*',
          Action: ['s3:GetObject', 's3:ListBucket'],
          Resource: 'arn:aws:s3:::${bucket}/*',
        },
      ],
    });
    assertEquals(response.policy.name, 'Public Read Updated');
    assertEquals(response.policy.updatedAt >= response.policy.createdAt, true);
  });

  it('should verify updated policy cascaded to attached buckets', async () => {
    const req = new TypedRequest<IReq_GetBucketPolicy>(url, 'getBucketPolicy');
    const response = await req.fire({ identity, bucketName: 'pol-bucket-1' });
    assertExists(response.policy);
    const parsed = JSON.parse(response.policy!);
    const sids = parsed.Statement.map((s: any) => s.Sid);
    assertEquals(sids.includes('PublicReadUpdated'), true);
  });

  it('should detach policy from bucket', async () => {
    const req = new TypedRequest<IReq_DetachPolicyFromBucket>(url, 'detachPolicyFromBucket');
    const response = await req.fire({ identity, policyId: policy2Id, bucketName: 'pol-bucket-1' });
    assertEquals(response.ok, true);
  });

  it('should verify bucket policy updated after detach', async () => {
    const req = new TypedRequest<IReq_GetBucketPolicy>(url, 'getBucketPolicy');
    const response = await req.fire({ identity, bucketName: 'pol-bucket-1' });
    assertExists(response.policy);
    const parsed = JSON.parse(response.policy!);
    const sids = parsed.Statement.map((s: any) => s.Sid);
    assertEquals(sids.includes('DenyDelete'), false);
    assertEquals(sids.includes('PublicReadUpdated'), true);
  });

  it('should delete a named policy', async () => {
    const req = new TypedRequest<IReq_DeleteNamedPolicy>(url, 'deleteNamedPolicy');
    const response = await req.fire({ identity, policyId: policy2Id });
    assertEquals(response.ok, true);
  });

  it('should handle bucket deletion cleaning up attachments', async () => {
    // Delete pol-bucket-1 which has policy1 attached
    const delBucket = new TypedRequest<IReq_DeleteBucket>(url, 'deleteBucket');
    await delBucket.fire({ identity, bucketName: 'pol-bucket-1' });

    // Verify policy1 no longer lists pol-bucket-1
    const req = new TypedRequest<IReq_GetPolicyBuckets>(url, 'getPolicyBuckets');
    const response = await req.fire({ identity, policyId: policy1Id });
    assertEquals(response.attachedBuckets.includes('pol-bucket-1'), false);
  });

  it('cleanup: delete remaining policy and bucket', async () => {
    const delPolicy = new TypedRequest<IReq_DeleteNamedPolicy>(url, 'deleteNamedPolicy');
    await delPolicy.fire({ identity, policyId: policy1Id });

    const listPolicies = new TypedRequest<IReq_ListNamedPolicies>(url, 'listNamedPolicies');
    const response = await listPolicies.fire({ identity });
    assertEquals(response.policies.length, 0);
  });
});
