#!/usr/bin/env python3
# Copyright 2026 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import argparse
import os
import shutil
import stat
import subprocess
import sys
import tempfile
import unittest
import zlib


class DevilUtilTest(unittest.TestCase):
  executable = None

  def setUp(self):
    self.temp_dir = tempfile.mkdtemp()
    self.addCleanup(shutil.rmtree, self.temp_dir)

  def run_util(self, *args, stdin=None):
    cmd = [self.executable] + list(args)
    return subprocess.run(
      cmd, capture_output=True, text=True, check=True, input=stdin
    )

  def test_hash(self):
    file1 = os.path.join(self.temp_dir, 'file1')
    content1 = b'hello world'
    with open(file1, 'wb') as f:
      f.write(content1)

    file2 = os.path.join(self.temp_dir, 'file2')
    content2 = b'foobar'
    with open(file2, 'wb') as f:
      f.write(content2)

    expected1 = hex(zlib.crc32(content1) & 0xFFFFFFFF)[2:]
    expected2 = hex(zlib.crc32(content2) & 0xFFFFFFFF)[2:]

    result = self.run_util('hash', f'{file1}:{file2}')
    self.assertEqual(result.stdout.strip(), f'{expected1}\n{expected2}')

  def test_hash_missing(self):
    missing_file = os.path.join(self.temp_dir, 'missing')
    result = self.run_util('hash', missing_file)
    self.assertEqual(result.stdout, '\n')

  def test_compress(self):
    dest = os.path.join(self.temp_dir, 'compressed.zst')
    content = 'test content\nline2'
    self.run_util('compress', dest, content)
    self.assertTrue(os.path.exists(dest))

    # Verify via response file expansion
    file1 = os.path.join(self.temp_dir, 'f1')
    with open(file1, 'w') as f:
      f.write('c1')

    # Compress the path of file1 so that @dest.zst expands to file1's path
    self.run_util('compress', dest, file1 + '\n')

    expected = hex(zlib.crc32(b'c1') & 0xFFFFFFFF)[2:]
    result = self.run_util('hash', f'@{dest}')
    self.assertEqual(result.stdout.strip(), expected)

  def test_archive_and_extract(self):
    src_dir = os.path.join(self.temp_dir, 'src')
    os.makedirs(src_dir)
    f1 = os.path.join(src_dir, 'file1')
    with open(f1, 'w') as f:
      f.write('content1')
    f2 = os.path.join(src_dir, 'file2')
    with open(f2, 'w') as f:
      f.write('content2')

    members_file = os.path.join(self.temp_dir, 'members')
    # Format is: host_path\narchive_path\n...
    with open(members_file, 'w') as f:
      f.write(f'{f1}\nsub/file1_in_arc\n')
      f.write(f'{f2}\nfile2_in_arc\n')

    archive = os.path.join(self.temp_dir, 'arc.zst')
    self.run_util('archive', archive, members_file)

    extract_dir = os.path.join(self.temp_dir, 'extract')
    os.makedirs(extract_dir)

    cwd = os.getcwd()
    os.chdir(extract_dir)
    try:
      self.run_util('extract', archive)
    finally:
      os.chdir(cwd)

    with open(os.path.join(extract_dir, 'sub/file1_in_arc'), 'r') as f:
      self.assertEqual(f.read(), 'content1')
    with open(os.path.join(extract_dir, 'file2_in_arc'), 'r') as f:
      self.assertEqual(f.read(), 'content2')

  def test_archive_and_extract_stdin_stdout(self):
    src_dir = os.path.join(self.temp_dir, 'src')
    os.makedirs(src_dir)
    f1 = os.path.join(src_dir, 'file1')
    with open(f1, 'w') as f:
      f.write('content1')

    members_file = os.path.join(self.temp_dir, 'members')
    with open(members_file, 'w') as f:
      f.write(f'{f1}\nfile1_in_arc\n')

    # Run archive to stdout
    cmd = [self.executable, 'archive', '-', members_file]
    result = subprocess.run(cmd, capture_output=True, check=True)
    archive_content = result.stdout

    # Run extract from stdin
    extract_dir = os.path.join(self.temp_dir, 'extract_stdin')
    os.makedirs(extract_dir)
    cwd = os.getcwd()
    os.chdir(extract_dir)
    try:
      subprocess.run(
        [self.executable, 'extract', '-'], input=archive_content, check=True
      )
    finally:
      os.chdir(cwd)

    with open(os.path.join(extract_dir, 'file1_in_arc'), 'r') as f:
      self.assertEqual(f.read(), 'content1')

  def test_pipe(self):
    pipe_path = os.path.join(self.temp_dir, 'test_pipe')
    self.run_util('pipe', pipe_path)
    self.assertTrue(os.path.exists(pipe_path))
    self.assertTrue(stat.S_ISFIFO(os.stat(pipe_path).st_mode))

  def test_response_file(self):
    file1 = os.path.join(self.temp_dir, 'f1')
    with open(file1, 'w') as f:
      f.write('c1')

    resp_file = os.path.join(self.temp_dir, 'resp')
    with open(resp_file, 'w') as f:
      f.write('hash\n')
      f.write(file1 + '\n')

    expected = hex(zlib.crc32(b'c1') & 0xFFFFFFFF)[2:]
    # Run without 'hash' command explicitly, it should come from resp file
    cmd = [self.executable, f'@{resp_file}']
    result = subprocess.run(cmd, capture_output=True, text=True, check=True)
    self.assertEqual(result.stdout.strip(), expected)

  def test_hash_with_parens(self):
    paren_dir = os.path.join(self.temp_dir, 'dir_(dbg)')
    os.makedirs(paren_dir)
    test_file = os.path.join(paren_dir, 'test.apk')
    content = b'content'
    with open(test_file, 'wb') as f:
      f.write(content)

    expected = hex(zlib.crc32(content) & 0xFFFFFFFF)[2:]
    result = self.run_util('hash', test_file)
    self.assertEqual(result.stdout.strip(), expected)


if __name__ == '__main__':
  parser = argparse.ArgumentParser('Tests for devil_util_bin')
  parser.add_argument('executable', help='Path to devil_util_bin')
  args, unknown = parser.parse_known_args()
  DevilUtilTest.executable = os.path.abspath(args.executable)

  unittest.main(argv=[sys.argv[0]] + unknown)
