<?php

namespace App\Services\FlowTester;

use Exception;

/**
 * 플로우 실행 엔진
 *
 * 플로우 정의를 받아 단계별로 실행하고 결과를 반환합니다.
 *
 * 실행 흐름:
 * 1. 플로우 정의 로드 및 검증
 * 2. 의존성 기반 실행 순서 결정 (TopSort)
 * 3. 변수 바인딩 초기화
 * 4. 각 단계 순차 실행
 * 5. 결과 수집 및 반환
 */
class FlowExecutor
{
    private VariableBinder $binder;

    private DependencyResolver $resolver;

    private ResponseValidator $validator;

    private HttpClient $httpClient;

    private ApiLogCapturer $logCapturer;

    private ConditionEvaluator $conditionEvaluator;

    /**
     * 실행 로그
     */
    private array $executionLog = [];

    /**
     * 실행 상태
     */
    private string $status = 'PENDING';

    /**
     * 현재 진행 상황
     */
    private int $completedSteps = 0;

    private int $totalSteps = 0;

    /**
     * 스텝별 성공/실패 추적
     */
    private array $stepSuccessMap = [];

    /**
     * 캡처된 API 로그
     */
    private array $apiLogs = [];

    /**
     * 플로우 설정 (bearerToken 동적 업데이트용)
     */
    private array $flowConfig = [];

    public function __construct(
        ?VariableBinder $binder = null,
        ?DependencyResolver $resolver = null,
        ?ResponseValidator $validator = null,
        ?HttpClient $httpClient = null,
        ?ApiLogCapturer $logCapturer = null,
        ?ConditionEvaluator $conditionEvaluator = null
    ) {
        $this->binder = $binder ?? new VariableBinder;
        $this->resolver = $resolver ?? new DependencyResolver;
        $this->validator = $validator ?? new ResponseValidator;
        $this->httpClient = $httpClient ?? new HttpClient;
        $this->logCapturer = $logCapturer ?? new ApiLogCapturer;
        $this->conditionEvaluator = $conditionEvaluator ?? new ConditionEvaluator($this->binder);
    }

    /**
     * 플로우 실행
     *
     * @param  array  $flowDefinition  플로우 정의 JSON
     * @param  array  $inputVariables  입력 변수 (실행 시 주입)
     * @return array 실행 결과
     */
    public function execute(array $flowDefinition, array $inputVariables = []): array
    {
        $startTime = microtime(true);
        $this->reset();

        // API 로그 캡처 시작
        $this->logCapturer->start();

        try {
            // 1. 플로우 정의 검증
            $this->validateFlowDefinition($flowDefinition);

            // 2. 설정 적용
            $this->applyConfig($flowDefinition['config'] ?? []);

            // 3. 전역 변수 초기화
            $variables = array_merge(
                $flowDefinition['variables'] ?? [],
                $inputVariables
            );
            $this->binder->setVariables($variables);

            // 4. 스텝 정보 추출
            $steps = $flowDefinition['steps'] ?? [];
            $this->totalSteps = count($steps);

            if ($this->totalSteps === 0) {
                return $this->buildResult($startTime, 'SUCCESS', 'No steps to execute');
            }

            // 5. 의존성 정렬
            $orderedStepIds = $this->resolver->resolve($steps);
            $stepMap = $this->buildStepMap($steps);

            // 6. 실행 상태 변경
            $this->status = 'RUNNING';

            // 7. 각 단계 실행
            $stopOnFailure = $flowDefinition['config']['stopOnFailure'] ?? true;

            foreach ($orderedStepIds as $stepId) {
                $step = $stepMap[$stepId];
                $stepName = $step['name'] ?? $stepId;

                // 1. 의존성 스텝 성공 여부 확인
                $dependencyCheck = $this->checkDependencies($step);
                if (! $dependencyCheck['canRun']) {
                    // 의존성 실패로 스킵
                    $skipResult = $this->buildStepResult($stepId, $stepName, microtime(true), false, [
                        'skipped' => true,
                        'skipReason' => $dependencyCheck['reason'],
                        'skipType' => 'dependency',
                        'failedDependencies' => $dependencyCheck['failedDeps'],
                    ]);
                    $this->executionLog[] = $skipResult;
                    $this->stepSuccessMap[$stepId] = false;
                    $this->registerStepResult($stepId, $skipResult);

                    continue;
                }

                // 2. 조건(condition) 평가 - 분기 처리
                if (isset($step['condition'])) {
                    $conditionResult = $this->conditionEvaluator->evaluate($step['condition']);

                    if (! $conditionResult['passed']) {
                        // 조건 불만족으로 스킵 (실패가 아님, 정상 스킵)
                        $skipResult = $this->buildStepResult($stepId, $stepName, microtime(true), true, [
                            'skipped' => true,
                            'skipReason' => 'Condition not met: '.$conditionResult['reason'],
                            'skipType' => 'condition',
                            'conditionEvaluation' => $conditionResult,
                        ]);
                        $this->executionLog[] = $skipResult;
                        // 조건 스킵은 성공으로 처리 (분기에서 선택되지 않은 것일 뿐)
                        $this->stepSuccessMap[$stepId] = true;
                        $this->completedSteps++;
                        $this->registerStepResult($stepId, $skipResult);

                        continue;
                    }
                }

                // 3. Bearer 토큰 동적 업데이트 (login 스텝 이후)
                $this->updateBearerToken();

                // 4. 스텝 실행
                $stepResult = $this->executeStep($step);

                $this->executionLog[] = $stepResult;
                $this->stepSuccessMap[$stepId] = $stepResult['success'];
                $this->registerStepResult($stepId, $stepResult);

                if ($stepResult['success']) {
                    $this->completedSteps++;
                } else {
                    if (! ($step['continueOnFailure'] ?? false) && $stopOnFailure) {
                        $this->status = 'FAILED';

                        return $this->buildResult(
                            $startTime,
                            'FAILED',
                            "Step '{$stepId}' failed: ".($stepResult['error'] ?? 'Unknown error'),
                            $stepId
                        );
                    }
                    // continueOnFailure가 true면 계속 진행
                }
            }

            // 8. 최종 상태 결정
            $this->status = $this->completedSteps === $this->totalSteps ? 'SUCCESS' : 'PARTIAL';

            return $this->buildResult($startTime, $this->status);
        } catch (Exception $e) {
            $this->status = 'FAILED';

            return $this->buildResult($startTime, 'FAILED', $e->getMessage());
        }
    }

    /**
     * 의존성 스텝 성공 여부 확인
     *
     * 조건부 의존성 지원:
     * - 단순 문자열: "step_id" - 해당 스텝이 성공해야 실행
     * - 조건부 객체: {"step": "step_id", "onlyIf": "success|failure|executed|skipped|any"}
     *
     * @param  array  $step  실행할 스텝
     * @return array ['canRun' => bool, 'reason' => string|null, 'failedDeps' => array]
     */
    private function checkDependencies(array $step): array
    {
        $dependencies = $step['dependsOn'] ?? [];

        if (empty($dependencies)) {
            return ['canRun' => true, 'reason' => null, 'failedDeps' => []];
        }

        $failedDeps = [];

        foreach ($dependencies as $dep) {
            // 조건부 의존성 파싱
            $parsed = $this->resolver->parseDependency($dep);
            $depId = $parsed['stepId'];
            $condition = $parsed['condition'];

            // 의존 스텝 결과 조회
            $stepResults = $this->conditionEvaluator->getStepResults();
            $depResult = $stepResults[$depId] ?? null;

            // 의존 스텝이 아직 실행되지 않은 경우 (순서 문제)
            if ($depResult === null && ! isset($this->stepSuccessMap[$depId])) {
                $failedDeps[] = "{$depId} (not executed yet)";

                continue;
            }

            // 조건에 따른 의존성 체크
            $conditionMet = $this->checkDependencyCondition($depId, $condition, $depResult);

            if (! $conditionMet) {
                $failedDeps[] = "{$depId} (condition '{$condition}' not met)";
            }
        }

        if (! empty($failedDeps)) {
            return [
                'canRun' => false,
                'reason' => 'Dependency condition failed: '.implode(', ', $failedDeps),
                'failedDeps' => $failedDeps,
            ];
        }

        return ['canRun' => true, 'reason' => null, 'failedDeps' => []];
    }

    /**
     * 의존성 조건 체크
     *
     * @param  string  $depId  의존 스텝 ID
     * @param  string  $condition  조건 (success, failure, executed, skipped, any)
     * @param  array|null  $depResult  의존 스텝 실행 결과
     * @return bool 조건 충족 여부
     */
    private function checkDependencyCondition(string $depId, string $condition, ?array $depResult): bool
    {
        // stepSuccessMap에서 기본 정보 조회
        $isSuccess = $this->stepSuccessMap[$depId] ?? false;
        $isSkipped = $depResult['skipped'] ?? false;
        $isExecuted = isset($this->stepSuccessMap[$depId]) && ! $isSkipped;

        return match ($condition) {
            'success' => $isSuccess && ! $isSkipped,
            'failure', 'failed' => ! $isSuccess && ! $isSkipped && $isExecuted,
            'executed' => $isExecuted,
            'skipped' => $isSkipped,
            'any' => true, // 순서만 보장, 결과 무관
            default => $isSuccess, // 기본값은 success
        };
    }

    /**
     * 단일 스텝 실행
     */
    private function executeStep(array $step): array
    {
        $stepId = $step['id'];
        $stepName = $step['name'] ?? $stepId;
        $startTime = microtime(true);

        try {
            // 0. useSessionAuth 옵션 처리 - 세션 인증 정보로 대체
            if (! empty($step['useSessionAuth'])) {
                return $this->executeSessionAuthStep($step, $stepId, $stepName, $startTime);
            }

            // 1. 딜레이 적용
            $delay = $step['delay'] ?? 0;
            if ($delay > 0) {
                usleep($delay * 1000); // ms → μs
            }

            // 2. 변수 바인딩
            $endpoint = $this->binder->bind($step['endpoint']);
            $headers = $this->binder->bind($step['headers'] ?? []);
            $body = $this->binder->bind($step['body'] ?? []);
            $query = $this->binder->bind($step['query'] ?? []);

            // 3. HTTP 요청 실행
            $method = strtoupper($step['method']);
            $response = $this->httpClient->request($method, $endpoint, [
                'headers' => $headers,
                'body' => $body,
                'query' => $query,
            ]);

            // 4. HTTP 에러 체크
            if ($response['error']) {
                return $this->buildStepResult($stepId, $stepName, $startTime, false, [
                    'error' => $response['error'],
                    'request' => [
                        'method' => $method,
                        'endpoint' => $endpoint,
                        'headers' => $headers,
                        'body' => $body,
                        'query' => $query,
                    ],
                ]);
            }

            // 5. 응답 검증 (expect 또는 assertions 둘 다 지원)
            $expect = $step['expect'] ?? [];
            if (empty($expect) && isset($step['assertions'])) {
                $expect = $this->convertAssertionsToExpect($step['assertions']);
            }

            // expect가 비어있으면 기본적으로 2xx 상태 코드 기대
            if (empty($expect) || ! isset($expect['status'])) {
                $expect['status'] = [200, 201, 204];
            }

            // expect 값에도 변수 바인딩 적용 (jsonPath 값의 {{변수}} 치환)
            $expect = $this->bindExpectVariables($expect);

            $validation = $this->validator->validate($response, $expect);

            // 6. 변수 추출
            $extracted = [];
            if (isset($step['extract'])) {
                $extracted = $this->validator->extractValues($response['body'], $step['extract']);
                $this->binder->setStepResult($stepId, $extracted, $response['body']);
            }

            // 7. 결과 구성
            $success = $validation['success'];
            // 성공/실패 이유 설명 생성
            $reason = $this->buildResultReason($success, $expect, $response['status'], $step);

            return $this->buildStepResult($stepId, $stepName, $startTime, $success, [
                'description' => $step['description'] ?? null,
                'reason' => $reason,
                'expect' => $expect,
                'request' => [
                    'method' => $method,
                    'endpoint' => $endpoint,
                    'headers' => $headers,
                    'body' => $body,
                    'query' => $query,
                ],
                'response' => [
                    'status' => $response['status'],
                    'body' => $response['body'],
                    'duration' => $response['duration'],
                ],
                'extracted' => $extracted,
                'validation' => $validation,
                'error' => $success ? null : implode('; ', $validation['errors']),
            ]);
        } catch (Exception $e) {
            return $this->buildStepResult($stepId, $stepName, $startTime, false, [
                'error' => $e->getMessage(),
            ]);
        }
    }

    /**
     * 세션 인증 스텝 실행
     *
     * useSessionAuth: true 옵션이 있는 login 스텝에서 사용
     * - 세션 토큰이 있으면 → 세션 토큰 사용 (API 호출 스킵)
     * - 세션 토큰이 없으면 → .env 크레덴셜로 실제 API 로그인
     *
     * @param  array  $step  스텝 정의
     * @param  string  $stepId  스텝 ID
     * @param  string  $stepName  스텝 이름
     * @param  float  $startTime  시작 시간
     * @return array 스텝 결과
     */
    private function executeSessionAuthStep(array $step, string $stepId, string $stepName, float $startTime): array
    {
        // 세션 인증 정보 조회
        $sessionAuth = $this->binder->getSessionAuth();

        // 세션 토큰이 있으면 사용 (API 호출 스킵)
        if (! empty($sessionAuth['token'])) {
            $extracted = [
                'token' => $sessionAuth['token'],
                'access_token' => $sessionAuth['token'],
            ];

            if ($sessionAuth['user']) {
                $extracted['user_id'] = $sessionAuth['user']['id'];
                $extracted['user_name'] = $sessionAuth['user']['name'];
                $extracted['user_email'] = $sessionAuth['user']['email'];
            }
            if ($sessionAuth['tenant_id']) {
                $extracted['tenant_id'] = $sessionAuth['tenant_id'];
            }

            $this->binder->setStepResult($stepId, $extracted, [
                'message' => '세션 인증 사용',
                'user' => $sessionAuth['user'],
                'tenant_id' => $sessionAuth['tenant_id'],
            ]);

            return $this->buildStepResult($stepId, $stepName, $startTime, true, [
                'description' => $step['description'] ?? '세션 인증 정보 사용',
                'reason' => '✓ 세션 인증 사용 (API 호출 생략)',
                'useSessionAuth' => true,
                'sessionUser' => $sessionAuth['user'],
                'extracted' => $extracted,
                'response' => [
                    'status' => 200,
                    'body' => [
                        'message' => '세션 인증 정보를 사용합니다.',
                        'access_token' => substr($sessionAuth['token'], 0, 20).'...',
                        'user' => $sessionAuth['user'],
                        'tenant_id' => $sessionAuth['tenant_id'],
                    ],
                    'duration' => 0,
                ],
            ]);
        }

        // 세션 토큰이 없으면 .env로 폴백하여 실제 API 로그인
        // useSessionAuth 옵션을 제거하고 일반 스텝처럼 실행
        $fallbackStep = $step;
        unset($fallbackStep['useSessionAuth']);
        $fallbackStep['description'] = '.env 크레덴셜로 API 로그인 (세션 토큰 없음)';

        return $this->executeLoginStep($fallbackStep, $stepId, $stepName, $startTime);
    }

    /**
     * 일반 로그인 스텝 실행 (실제 API 호출)
     *
     * @param  array  $step  스텝 정의
     * @param  string  $stepId  스텝 ID
     * @param  string  $stepName  스텝 이름
     * @param  float  $startTime  시작 시간
     * @return array 스텝 결과
     */
    private function executeLoginStep(array $step, string $stepId, string $stepName, float $startTime): array
    {
        try {
            // 딜레이 적용
            $delay = $step['delay'] ?? 0;
            if ($delay > 0) {
                usleep($delay * 1000);
            }

            // 변수 바인딩
            $endpoint = $this->binder->bind($step['endpoint']);
            $headers = $this->binder->bind($step['headers'] ?? []);
            $body = $this->binder->bind($step['body'] ?? []);
            $query = $this->binder->bind($step['query'] ?? []);

            // HTTP 요청 실행
            $method = strtoupper($step['method']);
            $response = $this->httpClient->request($method, $endpoint, [
                'headers' => $headers,
                'body' => $body,
                'query' => $query,
            ]);

            // HTTP 에러 체크
            if ($response['error']) {
                return $this->buildStepResult($stepId, $stepName, $startTime, false, [
                    'error' => $response['error'],
                    'reason' => '✗ .env 폴백 로그인 실패: '.$response['error'],
                    'request' => [
                        'method' => $method,
                        'endpoint' => $endpoint,
                        'headers' => $headers,
                        'body' => $body,
                    ],
                ]);
            }

            // 응답 검증
            $expect = $step['expect'] ?? ['status' => [200, 201]];
            $validation = $this->validator->validate($response, $expect);

            // 변수 추출
            $extracted = [];
            if (isset($step['extract'])) {
                $extracted = $this->validator->extractValues($response['body'], $step['extract']);
                $this->binder->setStepResult($stepId, $extracted, $response['body']);
            }

            $success = $validation['success'];

            return $this->buildStepResult($stepId, $stepName, $startTime, $success, [
                'description' => $step['description'] ?? '.env 크레덴셜로 API 로그인',
                'reason' => $success
                    ? '✓ .env 폴백 로그인 성공'
                    : '✗ .env 폴백 로그인 실패: '.implode('; ', $validation['errors']),
                'fallbackLogin' => true,
                'request' => [
                    'method' => $method,
                    'endpoint' => $endpoint,
                    'headers' => $headers,
                    'body' => $body,
                ],
                'response' => [
                    'status' => $response['status'],
                    'body' => $response['body'],
                    'duration' => $response['duration'],
                ],
                'extracted' => $extracted,
                'validation' => $validation,
                'error' => $success ? null : implode('; ', $validation['errors']),
            ]);
        } catch (Exception $e) {
            return $this->buildStepResult($stepId, $stepName, $startTime, false, [
                'error' => $e->getMessage(),
                'reason' => '✗ .env 폴백 로그인 예외: '.$e->getMessage(),
            ]);
        }
    }

    /**
     * 플로우 정의 검증
     */
    private function validateFlowDefinition(array $definition): void
    {
        // steps 필드 필수
        if (! isset($definition['steps']) || ! is_array($definition['steps'])) {
            throw new Exception('Flow definition must have a "steps" array');
        }

        // 각 스텝 검증
        foreach ($definition['steps'] as $index => $step) {
            if (! isset($step['id'])) {
                throw new Exception("Step at index {$index} must have an 'id' field");
            }
            if (! isset($step['method'])) {
                throw new Exception("Step '{$step['id']}' must have a 'method' field");
            }
            if (! isset($step['endpoint'])) {
                throw new Exception("Step '{$step['id']}' must have an 'endpoint' field");
            }
        }

        // 의존성 검증
        $validation = $this->resolver->validate($definition['steps']);
        if (! $validation['valid']) {
            throw new Exception('Dependency validation failed: '.implode('; ', $validation['errors']));
        }
    }

    /**
     * 설정 적용
     */
    private function applyConfig(array $config): void
    {
        // config 저장 (bearerToken 동적 업데이트용)
        $this->flowConfig = $config;
        // Base URL 결정 - JSON에 있으면 사용, 없으면 .env에서
        $baseUrl = $this->resolveBaseUrl($config['baseUrl'] ?? null);

        if ($baseUrl) {

            // Docker 환경에서 외부 URL을 내부 URL로 변환
            if ($this->isDockerEnvironment()) {
                $parsedUrl = parse_url($baseUrl);
                $host = $parsedUrl['host'] ?? '';

                // *.sam.kr 도메인을 nginx 컨테이너로 라우팅
                if (str_ends_with($host, '.sam.kr') || $host === 'sam.kr') {
                    $this->httpClient->setHostHeader($host);
                    $this->httpClient->withoutVerifying();

                    // URL을 https://nginx/... 로 변환
                    $internalUrl = preg_replace(
                        '#https?://[^/]+#',
                        'https://nginx',
                        $baseUrl
                    );
                    $baseUrl = $internalUrl;
                }
            }

            $this->httpClient->setBaseUrl($baseUrl);
        }

        // Timeout
        if (isset($config['timeout'])) {
            $timeout = (int) ($config['timeout'] / 1000); // ms → s
            $this->httpClient->setTimeout(max(1, $timeout));
        }

        // 기본 헤더
        if (isset($config['headers'])) {
            $this->httpClient->setDefaultHeaders($config['headers']);
        }

        // 인증 - JSON에 없으면 .env에서 자동 로드
        $apiKey = $config['apiKey'] ?? null;
        if (empty($apiKey)) {
            $apiKey = env('FLOW_TESTER_API_KEY');
        } else {
            // 플레이스홀더 치환 ({{$auth.apiKey}} 등)
            $apiKey = $this->binder->bind($apiKey);
        }
        if (! empty($apiKey)) {
            $this->httpClient->setApiKey($apiKey);
        }

        $bearerToken = $config['bearerToken'] ?? null;
        if (empty($bearerToken)) {
            // .env 또는 로그인 사용자의 토큰 사용
            $bearerToken = $this->getDefaultBearerToken();
        } else {
            // 플레이스홀더 치환 ({{$auth.token}} 등)
            $bearerToken = $this->binder->bind($bearerToken);
        }
        if (! empty($bearerToken)) {
            $this->httpClient->setBearerToken($bearerToken);
        }
    }

    /**
     * 기본 Bearer 토큰 조회
     *
     * 세션 토큰만 사용 (API 서버 로그인으로 발급받은 토큰)
     * .env 폴백은 MNG 토큰이 API 서버에서 인식되지 않으므로 제거됨
     */
    private function getDefaultBearerToken(): ?string
    {
        // 세션에 저장된 토큰 (API 서버 로그인으로 발급받은 토큰)
        return session('api_explorer_token') ?: null;
    }

    /**
     * Bearer 토큰 동적 업데이트
     *
     * login 스텝에서 추출된 token을 후속 요청에서 사용할 수 있도록
     * 각 스텝 실행 전에 config.bearerToken을 다시 바인딩합니다.
     */
    private function updateBearerToken(): void
    {
        $bearerToken = $this->flowConfig['bearerToken'] ?? null;

        if (! empty($bearerToken)) {
            // 플레이스홀더 치환 ({{login.token}} 등)
            $resolvedToken = $this->binder->bind($bearerToken);

            if (! empty($resolvedToken) && $resolvedToken !== $bearerToken) {
                // 바인딩된 토큰이 있으면 httpClient에 설정
                $this->httpClient->setBearerToken($resolvedToken);
            }
        }
    }

    /**
     * Base URL 결정
     *
     * 우선순위:
     * 1. JSON에 완전한 URL (도메인 포함) → 그대로 사용
     * 2. JSON에 경로만 있거나 없음 → .env의 FLOW_TESTER_API_BASE_URL + 경로
     *
     * @param  string|null  $configBaseUrl  JSON config의 baseUrl
     * @return string|null 최종 Base URL
     */
    private function resolveBaseUrl(?string $configBaseUrl): ?string
    {
        // 환경변수에서 기본 도메인 가져오기
        $envBaseUrl = env('FLOW_TESTER_API_BASE_URL');

        // JSON에 baseUrl이 없는 경우
        if (empty($configBaseUrl)) {
            return $envBaseUrl ?: null;
        }

        // JSON에 완전한 URL이 있는지 확인 (http:// 또는 https://로 시작)
        if (preg_match('#^https?://#', $configBaseUrl)) {
            // 완전한 URL이면 그대로 사용
            return $configBaseUrl;
        }

        // 상대 경로만 있는 경우 (.env 도메인 + 상대 경로)
        if ($envBaseUrl) {
            // .env URL 끝의 슬래시 제거
            $envBaseUrl = rtrim($envBaseUrl, '/');
            // 상대 경로 시작의 슬래시 보장
            $configBaseUrl = '/'.ltrim($configBaseUrl, '/');

            return $envBaseUrl.$configBaseUrl;
        }

        // .env도 없고 상대 경로만 있으면 그대로 반환 (실패할 수 있음)
        return $configBaseUrl;
    }

    /**
     * Docker 환경인지 확인
     */
    private function isDockerEnvironment(): bool
    {
        // Docker 컨테이너 내부에서 실행 중인지 확인
        return file_exists('/.dockerenv') || (getenv('DOCKER_CONTAINER') === 'true');
    }

    /**
     * 스텝 맵 구성 (id → step)
     */
    private function buildStepMap(array $steps): array
    {
        $map = [];
        foreach ($steps as $step) {
            $map[$step['id']] = $step;
        }

        return $map;
    }

    /**
     * 스텝 결과 구성
     */
    private function buildStepResult(
        string $stepId,
        string $stepName,
        float $startTime,
        bool $success,
        array $details = []
    ): array {
        $duration = (int) ((microtime(true) - $startTime) * 1000);

        return array_merge([
            'stepId' => $stepId,
            'stepName' => $stepName,
            'success' => $success,
            'duration' => $duration,
            'timestamp' => date('Y-m-d H:i:s'),
        ], $details);
    }

    /**
     * 최종 결과 구성
     */
    private function buildResult(
        float $startTime,
        string $status,
        ?string $errorMessage = null,
        ?string $failedStep = null
    ): array {
        $duration = (int) ((microtime(true) - $startTime) * 1000);

        // API 로그 캡처
        $this->apiLogs = $this->logCapturer->capture();

        return [
            'status' => $status,
            'duration' => $duration,
            'totalSteps' => $this->totalSteps,
            'completedSteps' => $this->completedSteps,
            'failedStep' => $failedStep,
            'errorMessage' => $errorMessage,
            'executionLog' => $this->executionLog,
            'apiLogs' => $this->apiLogs,
            'startedAt' => date('Y-m-d H:i:s', (int) $startTime),
            'completedAt' => date('Y-m-d H:i:s'),
        ];
    }

    /**
     * 상태 초기화
     */
    private function reset(): void
    {
        $this->executionLog = [];
        $this->status = 'PENDING';
        $this->completedSteps = 0;
        $this->totalSteps = 0;
        $this->stepSuccessMap = [];
        $this->apiLogs = [];
        $this->binder->reset();
        $this->conditionEvaluator->reset();
    }

    /**
     * 스텝 결과를 ConditionEvaluator에 등록
     *
     * 다음 스텝의 조건 평가에서 이전 스텝 결과를 참조할 수 있도록 합니다.
     *
     * @param  string  $stepId  스텝 ID
     * @param  array  $result  스텝 실행 결과
     */
    private function registerStepResult(string $stepId, array $result): void
    {
        $this->conditionEvaluator->setStepResult($stepId, $result);
    }

    /**
     * 현재 진행 상황 조회
     */
    public function getProgress(): array
    {
        return [
            'status' => $this->status,
            'completedSteps' => $this->completedSteps,
            'totalSteps' => $this->totalSteps,
            'percent' => $this->totalSteps > 0
                ? round(($this->completedSteps / $this->totalSteps) * 100, 1)
                : 0,
        ];
    }

    /**
     * 실행 로그 조회
     */
    public function getExecutionLog(): array
    {
        return $this->executionLog;
    }

    /**
     * 현재 상태 조회
     */
    public function getStatus(): string
    {
        return $this->status;
    }

    /**
     * 성공/실패 이유 설명 생성
     *
     * @param  bool  $success  성공 여부
     * @param  array  $expect  예상 조건
     * @param  int  $actualStatus  실제 HTTP 상태 코드
     * @param  array  $step  스텝 정의
     * @return string 이유 설명
     */
    private function buildResultReason(bool $success, array $expect, int $actualStatus, array $step): string
    {
        $expectedStatuses = $expect['status'] ?? [200, 201, 204];
        if (! is_array($expectedStatuses)) {
            $expectedStatuses = [$expectedStatuses];
        }

        // 예상 상태 코드 문자열 생성
        $expectedStr = implode(', ', $expectedStatuses);

        // 400, 404 등 에러 코드가 예상값인 경우 (부정 테스트)
        $isNegativeTest = ! empty(array_filter($expectedStatuses, fn ($s) => $s >= 400));

        if ($success) {
            if ($isNegativeTest) {
                // 부정 테스트 성공 설명
                $reason = "✓ 예상대로 {$actualStatus} 반환";
                if ($actualStatus === 400) {
                    $reason .= ' (유효성 검증 실패 확인)';
                } elseif ($actualStatus === 404) {
                    $reason .= ' (리소스 미존재 확인)';
                } elseif ($actualStatus === 409) {
                    $reason .= ' (충돌 상태 확인)';
                } elseif ($actualStatus === 403) {
                    $reason .= ' (권한 없음 확인)';
                }

                // 스텝 설명이 있으면 추가
                if (! empty($step['description'])) {
                    $reason .= ' - '.$step['description'];
                }

                return $reason;
            }

            // 일반 성공
            return "✓ 정상 응답 ({$actualStatus})";
        }

        // 실패 설명
        $reason = "✗ 예상: [{$expectedStr}], 실제: {$actualStatus}";

        return $reason;
    }

    /**
     * expect 값에 변수 바인딩 적용
     *
     * jsonPath 값들의 {{변수}} 플레이스홀더를 치환합니다.
     * 예: "$.data.client_code": "{{test_client_code}}" → "TEST_CLIENT_123"
     */
    private function bindExpectVariables(array $expect): array
    {
        // jsonPath 값들에 변수 바인딩 적용
        if (isset($expect['jsonPath']) && is_array($expect['jsonPath'])) {
            foreach ($expect['jsonPath'] as $path => $expected) {
                // 문자열이고 {{}} 플레이스홀더가 있는 경우만 바인딩
                if (is_string($expected) && str_contains($expected, '{{')) {
                    $expect['jsonPath'][$path] = $this->binder->bind($expected);
                }
            }
        }

        return $expect;
    }

    /**
     * assertions 배열 형식을 expect 객체 형식으로 변환
     *
     * Postman 스타일:
     * [
     *   {"type": "status", "expected": 200},
     *   {"type": "jsonPath", "path": "data.id", "operator": "exists"},
     *   {"type": "jsonPath", "path": "data.name", "expected": "테스트"}
     * ]
     *
     * → 내부 형식:
     * {
     *   "status": 200,
     *   "jsonPath": {
     *     "data.id": "@exists",
     *     "data.name": "테스트"
     *   }
     * }
     */
    private function convertAssertionsToExpect(array $assertions): array
    {
        $expect = [];

        foreach ($assertions as $assertion) {
            $type = $assertion['type'] ?? '';

            switch ($type) {
                case 'status':
                    $expect['status'] = $assertion['expected'] ?? 200;
                    break;

                case 'jsonPath':
                    $path = $assertion['path'] ?? '';
                    if (empty($path)) {
                        continue 2;
                    }

                    if (! isset($expect['jsonPath'])) {
                        $expect['jsonPath'] = [];
                    }

                    // operator가 있으면 @operator 형식으로 변환
                    if (isset($assertion['operator'])) {
                        $expect['jsonPath'][$path] = '@'.$assertion['operator'];
                    } elseif (isset($assertion['expected'])) {
                        // expected 값이 있으면 직접 비교
                        $expect['jsonPath'][$path] = $assertion['expected'];
                    }
                    break;
            }
        }

        return $expect;
    }
}
