<?php

use App\Services\Env\EnvFileEditor;
use App\Services\Env\InvalidEnvContentException;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;

/**
 * EnvFileEditor is the net under App\Livewire\Admin\Integrations' raw .env
 * editor (Part B of the integrations-page work): validate before writing,
 * back up before every write that actually happens, never touch a file that
 * failed either check.
 *
 * Every test here points the editor at a throwaway path under
 * storage/framework/testing — never base_path('.env'), the real file this dev
 * machine boots from.
 */
function envEditorPath(): string
{
    $path = storage_path('framework/testing/env-file-editor-'.Str::random(12).'.env');
    File::ensureDirectoryExists(dirname($path));

    return $path;
}

afterEach(function () {
    // Backups are deliberately never pruned by the class under test (that is
    // the point — see the console page's own warning) — but a test run is
    // not the installation the warning is about, so it cleans up after itself.
    foreach (glob(storage_path('framework/testing/env-file-editor-*')) as $leftover) {
        @unlink($leftover);
    }
});

it('accepts blank lines, comments, and KEY=value, and nothing else', function () {
    $editor = new EnvFileEditor(envEditorPath());

    expect($editor->firstInvalidLine("APP_NAME=CluPilot\n\n# a comment\nDB_HOST=mariadb\n"))->toBeNull()
        ->and($editor->isValid("APP_NAME=CluPilot\n"))->toBeTrue()
        // A value may itself contain '=' — only the first one delimits the key.
        ->and($editor->isValid('MIX=a=b=c'))->toBeTrue()
        // A key may have an empty value.
        ->and($editor->isValid('EMPTY_OK='))->toBeTrue();

    // Neither blank, nor a comment, nor KEY=value — the exact line named in
    // the task, and 1-indexed so it can be shown to an operator directly.
    expect($editor->firstInvalidLine("APP_NAME=CluPilot\nthis is not env syntax\nDB_HOST=mariadb\n"))->toBe(2)
        ->and($editor->isValid("APP_NAME=CluPilot\nthis is not env syntax\n"))->toBeFalse();
});

it('refuses to write invalid content, and never touches the file', function () {
    // Mutation target: an invalid file is refused rather than written.
    $path = envEditorPath();
    File::put($path, "ORIGINAL=untouched\n");
    $editor = new EnvFileEditor($path);

    expect(fn () => $editor->write("ORIGINAL=untouched\nnot env syntax at all\n"))
        ->toThrow(InvalidEnvContentException::class);

    // The file on disk is EXACTLY what it was before the rejected write —
    // not merely "still has ORIGINAL in it somewhere".
    expect(File::get($path))->toBe("ORIGINAL=untouched\n")
        // And no backup was created either: nothing was about to change.
        ->and(glob($path.'.bak-*'))->toBe([]);
});

it('refuses an empty file even though it is syntactically valid', function () {
    $path = envEditorPath();
    File::put($path, "ORIGINAL=untouched\n");
    $editor = new EnvFileEditor($path);

    expect(fn () => $editor->write(''))->toThrow(InvalidEnvContentException::class)
        ->and(fn () => $editor->write("   \n\n"))->toThrow(InvalidEnvContentException::class);

    expect(File::get($path))->toBe("ORIGINAL=untouched\n");
});

it('backs up the previous content before writing the new content', function () {
    // Mutation target: a backup really exists after a save.
    $path = envEditorPath();
    File::put($path, "OLD_KEY=old-value\n");
    $editor = new EnvFileEditor($path);

    $backupPath = $editor->write("NEW_KEY=new-value\n");

    expect(File::exists($backupPath))->toBeTrue()
        ->and(File::get($backupPath))->toBe("OLD_KEY=old-value\n")
        // The backup sits BESIDE the file, under the same name plus a suffix
        // — not in some other directory an operator would have to be told
        // about separately.
        ->and(dirname($backupPath))->toBe(dirname($path))
        ->and(File::get($path))->toBe("NEW_KEY=new-value\n");
});

it('does not collide when two writes land in the same second', function () {
    $path = envEditorPath();
    File::put($path, "V=1\n");
    $editor = new EnvFileEditor($path);

    $first = $editor->write("V=2\n");
    $second = $editor->write("V=3\n");

    expect($first)->not->toBe($second)
        ->and(File::exists($first))->toBeTrue()
        ->and(File::exists($second))->toBeTrue();
});

it('creates the file when none existed before, without trying to back up something that was never there', function () {
    // Codex review, P2: File::copy() on a source that does not exist throws,
    // which made the first save on an installation with no .env yet fail
    // outright instead of creating one.
    $path = envEditorPath();
    File::delete($path); // envEditorPath() seeds it — deliberately undone here.
    $editor = new EnvFileEditor($path);

    $backup = $editor->write("FRESH=value\n");

    expect($backup)->toBe('')
        ->and(File::get($path))->toBe("FRESH=value\n");
});

it('answers empty rather than throwing when asked to back up a file that does not exist', function () {
    $path = envEditorPath();
    File::delete($path);

    expect((new EnvFileEditor($path))->backup())->toBe('');
});

it('defaults to base_path(.env) when no path is given', function () {
    expect((new EnvFileEditor)->path())->toBe(base_path('.env'));
});

it('reads empty rather than failing when the file does not exist yet', function () {
    $editor = new EnvFileEditor(envEditorPath());

    expect($editor->read())->toBe('');
});
