import { AssumeRoleCommand, STSClient } from '@aws-sdk/client-sts'
import { beforeAll, describe, expect, it } from 'vitest'
import { S3mini } from '../../src/s3-client/S3mini.js'
import { createSigV4Signer } from '../../src/s3-client/signer.js'
import { randomBytes } from '../test-utils/browser-crypto.js'
import { accessKeyId, getConfig, secretAccessKey } from './config.js'

// @ts-expect-error todo
const config = getConfig(import.meta.env)

const suiteName = 'MinIO S3 client tests'

if (config) {
  const { endpoint, region } = config
  const presigner = createSigV4Signer({
    accessKeyId,
    secretAccessKey,
    region,
    endpoint,
  })

  const s3client = new S3mini({
    endpoint,
    region,
    signRequest: presigner,
  })

  const EIGHT_MB = 8 * 1024 * 1024
  const key_bin = 'test-multipart.bin'

  const large_buffer = randomBytes(EIGHT_MB * 3.2)
  const content = 'some content'
  const key = 'first-test-object.txt'
  const key_list_parts = 'test-list-parts.bin'
  const key_abort_multipart = 'test-abort-multipart.bin'

  const FILE_KEYS = [key, key_bin, key_list_parts, key_abort_multipart]

  beforeAll(async () => {
    for (const key of FILE_KEYS) {
      try {
        await s3client.deleteObject({ key })
      } catch {
        // intentionally ignore the errors as the objects don't exists. still feels hacky tbh
      }
    }
  })

  /** Create STS client using @aws-sdk/client-sts */
  function createSTSClient({ endpoint, accessKeyId, secretAccessKey, region }) {
    return new STSClient({
      region,
      endpoint,
      credentials: { accessKeyId, secretAccessKey },
    })
  }

  /** Get temporary credentials via AssumeRole */
  async function assumeRole(
    stsClient: STSClient,
    { durationSeconds = 900 } = {},
  ) {
    const response = await stsClient.send(
      new AssumeRoleCommand({
        RoleArn: 'aws:iam::000000000000:role/test-role', // MinIO doesn't validate ARN
        RoleSessionName: 'uppy-test',
        DurationSeconds: durationSeconds,
      }),
    )
    const creds = response.Credentials!
    return {
      AccessKeyId: creds.AccessKeyId,
      SecretAccessKey: creds.SecretAccessKey,
      SessionToken: creds.SessionToken,
      Expiration: creds.Expiration?.toISOString() || creds.Expiration,
    }
  }

  // ===== signRequest tests =====

  describe(suiteName, () => {
    it('simple putObject upload', async () => {
      const key = 'presigned-test-file.txt'
      const fileContents = new TextEncoder().encode(
        'Hello from pre-signed URL test.',
      )

      const result = await s3client.putObject({
        key,
        data: fileContents,
        fileType: 'text/plain',
      })

      expect(result.location).toBeDefined()
      expect(result.location).toContain(key)
      // location should be a clean URL without query string (no signing params)
      expect(result.location).not.toContain('X-Amz-Signature')
      expect(result.location).not.toContain('?')
    })

    it('multipart upload with signRequest', async () => {
      const key = `presigned-multipart-${Date.now()}.bin`
      const partSize = 5 * 1024 * 1024 // 5MB
      const part = randomBytes(partSize)

      const { uploadId } = await s3client.createMultipartUpload({
        key,
        fileType: 'application/octet-stream',
      })
      expect(uploadId).toBeDefined()

      const partNumber = 1
      const { etag } = await s3client.uploadPart({
        key,
        uploadId,
        data: part,
        partNumber,
      })
      expect(etag).toBeDefined()

      const result = await s3client.completeMultipartUpload({
        key,
        uploadId,
        parts: [{ etag, partNumber }],
      })
      expect(result.etag).toBeDefined()
      expect(result.location).toBeDefined()
      expect(result.location).toContain(key)
      expect(result.key).toBe(key)

      await s3client.deleteObject({ key })
    })

    // ===== getCredentials tests (STS) =====

    describe('getCredentials (STS)', () => {
      const stsEndpoint = new URL(endpoint).origin

      it('should upload using getCredentials callback', async () => {
        const testKey = `sts-getcreds-${Date.now()}.txt`

        const s3 = new S3mini({
          endpoint,
          region,
          getCredentials: async () => {
            const stsClient = createSTSClient({
              endpoint: stsEndpoint,
              accessKeyId,
              secretAccessKey,
              region,
            })
            const creds = await assumeRole(stsClient)
            return {
              credentials: {
                accessKeyId: creds.AccessKeyId!,
                secretAccessKey: creds.SecretAccessKey!,
                sessionToken: creds.SessionToken!,
                expiration: creds.Expiration as string,
              },
              bucket: new URL(endpoint).pathname.slice(1),
              region,
            }
          },
        })

        try {
          const result = await s3.putObject({
            key: testKey,
            data: 'Hello STS!',
            fileType: 'text/plain',
          })
          expect(result.location).toBeDefined()
          expect(result.location).toContain(testKey)
          expect(result.location).not.toContain('X-Amz-Signature')
          expect(result.location).not.toContain('?')
        } finally {
          await s3.deleteObject({ key: testKey })
        }
      })

      it('should cache credentials across multiple requests', async () => {
        let fetchCount = 0

        const s3 = new S3mini({
          endpoint,
          region,
          getCredentials: async () => {
            fetchCount++
            const stsClient = createSTSClient({
              endpoint: stsEndpoint,
              accessKeyId,
              secretAccessKey,
              region,
            })
            const creds = await assumeRole(stsClient)
            return {
              credentials: {
                accessKeyId: creds.AccessKeyId!,
                secretAccessKey: creds.SecretAccessKey!,
                sessionToken: creds.SessionToken!,
                expiration: creds.Expiration as string,
              },
              bucket: new URL(endpoint).pathname.slice(1),
              region,
            }
          },
        })

        await s3.putObject({
          key: `sts-cache-1-${Date.now()}.txt`,
          data: 'File 1',
          fileType: 'text/plain',
        })
        await s3.putObject({
          key: `sts-cache-2-${Date.now()}.txt`,
          data: 'File 2',
          fileType: 'text/plain',
        })
        expect(fetchCount).toBe(1)
      })

      it('should perform multipart upload with getCredentials', async () => {
        const s3 = new S3mini({
          endpoint,
          region,
          getCredentials: async () => {
            const stsClient = createSTSClient({
              endpoint: stsEndpoint,
              accessKeyId,
              secretAccessKey,
              region,
            })
            const creds = await assumeRole(stsClient)
            return {
              credentials: {
                accessKeyId: creds.AccessKeyId!,
                secretAccessKey: creds.SecretAccessKey!,
                sessionToken: creds.SessionToken!,
                expiration: creds.Expiration as string,
              },
              bucket: new URL(endpoint).pathname.slice(1),
              region,
            }
          },
        })

        const key = `sts-multipart-${Date.now()}.bin`
        const partSize = 5 * 1024 * 1024 // 5MB
        const part = randomBytes(partSize)

        const { uploadId } = await s3.createMultipartUpload({
          key,
          fileType: 'application/octet-stream',
        })
        expect(uploadId).toBeDefined()

        const partNumber = 1
        const { etag } = await s3.uploadPart({
          key,
          uploadId,
          data: part,
          partNumber,
        })
        expect(etag).toBeDefined()

        const result = await s3.completeMultipartUpload({
          key,
          uploadId,
          parts: [{ etag, partNumber }],
        })
        expect(result.etag).toBeDefined()
        expect(result.location).toBeDefined()
        expect(result.location).toContain(key)
        expect(result.key).toBe(key)

        await s3.deleteObject({ key })
      })
    })

    it('instantiates s3client', () => {
      expect(s3client).toBeInstanceOf(S3mini) // ← updated expectation
    })

    // we don't need an explicit eTag method as we already get eTag in the putOject response
    it('putObject uploads successfully and returns ETag', async () => {
      const response = await s3client.putObject({
        key,
        data: content,
        fileType: 'text/plain',
      })
      expect(response.etag).toBeDefined()
      await s3client.deleteObject({ key })
    })

    it('putObject handles binary data', async () => {
      const binaryData = new Uint8Array(6).fill(0xff)

      const response = await s3client.putObject({
        key,
        data: binaryData,
        fileType: 'application/octet-stream',
      })

      expect(response).toBeDefined()
      expect(response.etag).toBeDefined()

      // cleanup

      await s3client.deleteObject({ key })
    })

    // test createMultipartUpload

    it('createMultipartUpload returns a valid uploadId', async () => {
      const { uploadId } = await s3client.createMultipartUpload({
        key: key_bin,
        fileType: 'application/octet-stream',
      })
      expect(uploadId).toBeDefined()
      expect(typeof uploadId).toBe('string')
      expect(uploadId.length).toBeGreaterThan(0)

      // cleanup
      await s3client.abortMultipartUpload({ key, uploadId })
    })

    // test uploadPart

    it('uploadPart returns partNumber and Etag', async () => {
      const partData = randomBytes(EIGHT_MB)

      const { uploadId } = await s3client.createMultipartUpload({
        key: key_bin,
        fileType: 'application/octet-stream',
      })

      // upload part
      const partResult = await s3client.uploadPart({
        key: key_bin,
        uploadId,
        data: partData,
        partNumber: 1,
      })

      expect(partResult).toBeDefined()
      expect(partResult.etag).toBeDefined()
      expect(typeof partResult.etag).toBe('string')
      expect(partResult.etag.length).toBe(32)

      // cleanup
      await s3client.abortMultipartUpload({ key, uploadId })
    })

    // end to end multipart flow

    it('completeMultipartUpload assembles parts correctly', async () => {
      // key - key_bin
      const partSize = EIGHT_MB
      const totalParts = Math.ceil(large_buffer.byteLength / partSize)

      const { uploadId } = await s3client.createMultipartUpload({
        key: key_bin,
        fileType: 'application/octet-stream',
      })
      expect(uploadId).toBeDefined()

      // upload all parts
      const uploadPromises: Promise<{ etag: string }>[] = []
      for (let i = 0; i < totalParts; i++) {
        const partBuffer = large_buffer.subarray(
          i * partSize,
          (i + 1) * partSize,
        )
        uploadPromises.push(
          s3client.uploadPart({
            key: key_bin,
            uploadId,
            data: partBuffer,
            partNumber: i + 1,
          }),
        )
      }
      const uploadResponses = await Promise.all(uploadPromises)

      // verify all parts uploaded succesfully

      expect(uploadResponses.length).toBe(totalParts)

      uploadResponses.forEach((response) => {
        expect(response.etag).toBeDefined()
      })

      // create multipart upload

      const parts = uploadResponses.map((response, index) => ({
        partNumber: index + 1,
        etag: response.etag,
      }))

      const completeResponse = await s3client.completeMultipartUpload({
        key: key_bin,
        uploadId,
        parts,
      })

      expect(completeResponse).toBeDefined()
      expect(typeof completeResponse).toBe('object')
      expect(completeResponse.etag).toBeDefined()
      expect(typeof completeResponse.etag).toBe('string')
      expect(completeResponse.etag!.length).toBe(32 + 2)

      // cleanup

      await s3client.deleteObject({ key: key_bin })
    })

    it('abortMultipartUpload cancels upload successfully', async () => {
      // start upload
      const { uploadId } = await s3client.createMultipartUpload({
        key: key_abort_multipart,
        fileType: 'application/octet-stream',
      })

      expect(uploadId).toBeDefined()

      const partData = randomBytes(EIGHT_MB)
      await s3client.uploadPart({
        key: key_abort_multipart,
        uploadId,
        data: partData,
        partNumber: 1,
      })

      // abort
      await s3client.abortMultipartUpload({
        key: key_abort_multipart,
        uploadId,
      })
    })

    it('listParts returns uploaded parts correctly', async () => {
      const partSize = EIGHT_MB

      const { uploadId } = await s3client.createMultipartUpload({
        key: key_list_parts,
        fileType: 'application/octet-stream',
      })
      expect(uploadId).toBeDefined()

      const part1Data = randomBytes(partSize)
      const part2Data = randomBytes(partSize)

      const part1Result = await s3client.uploadPart({
        key: key_list_parts,
        uploadId,
        data: part1Data,
        partNumber: 1,
      })
      const part2Result = await s3client.uploadPart({
        key: key_list_parts,
        uploadId,
        data: part2Data,
        partNumber: 2,
      })

      const parts = await s3client.listParts({
        uploadId,
        key: key_list_parts,
      })

      expect(parts).toBeInstanceOf(Array)
      expect(parts.length).toBe(2)

      // verify part 1
      expect(parts[0].partNumber).toBe(1)
      expect(parts[0].etag).toBe(part1Result.etag)

      // verify part 2
      expect(parts[1].partNumber).toBe(2)
      expect(parts[1].etag).toBe(part2Result.etag)

      // cleanup abort upload
      await s3client.abortMultipartUpload({ key, uploadId })
    })
  })
} else {
  console.warn(
    'Skipping MinIO S3 client tests: missing env variable VITE_MINIO_CONFIG',
  )
  describe.skip(suiteName, () => {
    it('noop', () => {})
  })
}
