// Copyright 2025 The Jujutsu Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#![cfg(unix)] // Nothing to test on Windows

use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::sync::Arc;

use jj_lib::backend::TreeValue;
use jj_lib::commit::Commit;
use jj_lib::merged_tree::MergedTree;
use jj_lib::repo::Repo as _;
use jj_lib::repo_path::RepoPath;
use jj_lib::store::Store;
use pollster::FutureExt as _;
use testutils::TestResult;
use testutils::TestTreeBuilder;
use testutils::TestWorkspace;
use testutils::repo_path;

/// Assert that a file's executable bit matches the expected value.
#[track_caller]
fn assert_file_executable(path: &Path, expected: bool) {
    let perms = path.metadata().unwrap().permissions();
    let actual = (perms.mode() & 0o100) == 0o100;
    assert_eq!(actual, expected);
}

/// Set the executable bit of a file on the filesystem.
#[track_caller]
fn set_file_executable(path: &Path, executable: bool) {
    let prev_mode = path.metadata().unwrap().permissions().mode();
    let is_executable = prev_mode & 0o100 != 0;
    assert_ne!(executable, is_executable, "why are you calling this?");
    let new_mode = if executable { 0o755 } else { 0o644 };
    fs::set_permissions(path, PermissionsExt::from_mode(new_mode)).unwrap();
}

/// Assert that a tree value's executable bit matches the expected value.
#[track_caller]
fn assert_tree_executable(tree_val: TreeValue, expected: bool) {
    if let TreeValue::File { executable, .. } = tree_val {
        assert_eq!(executable, expected);
    } else {
        panic!()
    }
}

/// Create a tree with an empty file having the given executable bit. Returns
/// the new tree id.
#[track_caller]
fn create_tree_executable(
    store: &Arc<Store>,
    repo_path: &RepoPath,
    executable: bool,
) -> MergedTree {
    let mut tree_builder = TestTreeBuilder::new(store.clone());
    tree_builder.file(repo_path, "").executable(executable);
    tree_builder.write_merged_tree()
}

/// Build two commits that write the executable bit of a file as true/false.
#[track_caller]
fn prepare_exec_commits(ws: &TestWorkspace, repo_path: &RepoPath) -> (Commit, Commit) {
    let store = ws.repo.store();
    let tree_exec = create_tree_executable(store, repo_path, true);
    let tree_no_exec = create_tree_executable(store, repo_path, false);

    let commit_with_id = |id| testutils::commit_with_tree(ws.repo.store(), id);
    let commit_exec = commit_with_id(tree_exec);
    let commit_no_exec = commit_with_id(tree_no_exec);
    assert_ne!(commit_exec, commit_no_exec);

    (commit_exec, commit_no_exec)
}

/// Test that checking out a tree writes the correct executable bit to the
/// filesystem.
#[test]
fn test_exec_bit_checkout() -> TestResult {
    let mut ws = TestWorkspace::init();
    let path = &ws.workspace.workspace_root().join("file");
    let repo_path = repo_path("file");

    let (exec, no_exec) = prepare_exec_commits(&ws, repo_path);
    let mut checkout_exec_commit = |executable| {
        let commit = if executable { &exec } else { &no_exec };
        let op_id = ws.repo.op_id().clone();
        ws.workspace
            .check_out(op_id, None, commit)
            .block_on()
            .unwrap();
    };

    // Checkout commits and ensure the filesystem is updated correctly.
    assert!(!fs::exists(path)?);
    for exec in [true, false, true] {
        checkout_exec_commit(exec);
        assert_file_executable(path, exec);
    }
    Ok(())
}

/// Test that snapshotting files stores the correct executable bit in the tree.
#[test]
fn test_exec_bit_snapshot() -> TestResult {
    let mut ws = TestWorkspace::init();
    let path = &ws.workspace.workspace_root().join("file");
    let repo_path = repo_path("file");

    // Snapshot, then assert the tree has the expected executable bit.
    let mut snapshot_assert_exec_bit = |expected| {
        let merged_tree_val = ws
            .snapshot()
            .unwrap()
            .path_value(repo_path)
            .block_on()
            .unwrap();
        let tree_val = merged_tree_val.into_resolved().unwrap().unwrap();
        assert_tree_executable(tree_val, expected);
    };

    // Snapshot tree values when the file is/isn't executable.
    fs::write(path, "initial content")?;
    snapshot_assert_exec_bit(false);

    fs::write(path, "first change")?;
    snapshot_assert_exec_bit(false);

    set_file_executable(path, true);
    snapshot_assert_exec_bit(true);

    fs::write(path, "second change")?;
    snapshot_assert_exec_bit(true);

    // Back to the same contents as before, but different exec bit.
    fs::write(path, "first change")?;
    set_file_executable(path, false);
    snapshot_assert_exec_bit(false);

    set_file_executable(path, true);
    snapshot_assert_exec_bit(true);
    Ok(())
}
