"""
QA Test Automation AI Core Service

This module implements the core AI functionality for:
1. Generating BDD test cases from acceptance criteria
2. Generating hierarchical module/suite/test case structures
"""

import json
import logging
import os
from typing import Dict, List, Optional, Union

from openai import OpenAI
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
from dotenv import load_dotenv
from fastapi.middleware.cors import CORSMiddleware

load_dotenv()

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

app = FastAPI(title="QA Test Automation AI Service")
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

client = OpenAI(api_key = os.getenv("OPENAI_API_KEY"))
LLM_MODEL = os.getenv("LLM_MODEL", "gpt-4o-mini")

# --- Data Models ---

class TestCaseGenerationRequest(BaseModel):
    """Request model for generating a single test case"""
    acceptance_criteria: str = Field(default=None, description="The acceptance criteria in natural language")
    project_context: Dict = Field(default={}, description="Project, module and suite context")
    tags: Optional[str] = Field(default=None, description="Optional tags for the test case")
    bdd: Dict = Field(default=None, description="Structured BDD object for code generation")
    language: str = Field(default="javascript", description="Target language for code export")


class TestStructureGenerationRequest(BaseModel):
    """Request model for generating module + test suite + test cases structure"""
    scenario_description: str = Field(..., description="Brief test scenario description")
    project_id: str = Field(..., description="ID of the project")
    project_name: Optional[str] = None


class FeedbackRequest(BaseModel):
    """Model for collecting feedback on generated tests"""
    original_input: str
    generated_output: Dict
    rating: int = Field(..., ge=1, le=5)
    comments: Optional[str] = None
    corrections: Optional[Dict] = None

class AIOutputValidationError(Exception):
    """Raised when AI output is not usable"""
    pass

class InsufficientContentError(Exception):
    """Raised when AI output lacks required content"""
    pass


class MultipleTestData(BaseModel):
    """Model for multiple test data variants with flexible fields"""
    title: str = Field(..., description="Title/name for this test variant")
    
    class Config:
        extra = "allow"

    def __getitem__(self, key):
        """Allow dict-like access to extra fields"""
        return getattr(self, key, None)
    
    def get(self, key, default=None):
        """Allow dict-like get method"""
        return getattr(self, key, default)

class PlaywrightGenerationRequest(BaseModel):
    """Request model for generating Playwright test code using LLM"""
    test_name: str = Field(..., description="Name for the generated test")
    test_steps: List[Dict] = Field(..., description="List of test steps with actions and selectors")
    multipleTest: List[MultipleTestData] = Field(..., description="List of test data variants")
    language: str = Field(default="javascript", description="Target language (javascript/python/java/csharp)")
    page_url: Optional[str] = Field(default=None, description="Base URL for the test")
    additional_context: Optional[str] = Field(default=None, description="Additional context or requirements")

# --- Core Logic for Test Case Generation ---
def validate_bdd_output(structured_bdd: Dict) -> None:
    """
    Validate that the BDD output contains usable scenarios
    """
    if not structured_bdd.get("scenarios"):
        raise InsufficientContentError("No test scenarios were generated")
    
    valid_scenarios = []
    for scenario in structured_bdd["scenarios"]:
        if (scenario.get("name") and 
            (scenario.get("given") or scenario.get("when") or scenario.get("then"))):
            valid_scenarios.append(scenario)
    
    if not valid_scenarios:
        raise InsufficientContentError("Generated scenarios lack proper Given-When-Then structure")
    
    structured_bdd["scenarios"] = valid_scenarios


def generate_test_case_prompt(acceptance_criteria: str, project_context: Dict, tags: Optional[str] = None) -> str:
    """
    Create an optimized prompt for the LLM to generate BDD test cases
    """
    context_str = f"Project: {project_context.get('project_name', 'Unknown')}"
    if 'module_name' in project_context:
        context_str += f"\nModule: {project_context['module_name']}"
    if 'suite_name' in project_context:
        context_str += f"\nTest Suite: {project_context['suite_name']}"
    
    tags_str = tags if tags else "None"
    
    prompt = f"""
You are a senior QA automation engineer embedded in a cross-functional Agile team. 
Your task is to convert business requirements — provided as user stories, acceptance criteria, or product descriptions — 
into comprehensive, executable BDD-style test scenarios.

----------------------------------------------------------------------
### Core Objectives
----------------------------------------------------------------------
Produce *complete, executable Gherkin* for SpecFlow/Cucumber/Behave that:
1. Reflect full business intent — not just one "happy path".
2. Capture *all relevant rules, exclusions, and non-functional criteria*.
3. Generate scenarios that are:
   - **Business-relevant** (speak in stakeholder language)
   - **Automation-ready** (directly runnable in API/UI testing)
   - **Implementation-agnostic** (no code or DOM specifics)
   - **Consistent, reusable, and complete**

----------------------------------------------------------------------
### Domain Detection Logic
----------------------------------------------------------------------

**If the input mentions** terms like:
- "endpoint", "API", "service", "response", "request", "HTTP", "token", "authorization", "payload", "JSON", "header", "status code"
→ Treat as **API context**.

**If the input mentions** terms like:
- "screen", "form", "button", "page", "field", "navigation", "UI", "component", "modal", "user clicks", "dashboard"
→ Treat as **UI context**.

**If both appear**, treat as **mixed (end-to-end)** and combine both contexts.

----------------------------------------------------------------------
### Output Format (Strict)
----------------------------------------------------------------------

**Feature Section**
- Feature: [Business capability or API under test]
- One or two-line description summarizing business value.

**Background Section (optional but encouraged)**
- For **API features**, include:
  Given the API base URL is "<baseUrl>"
  And I set header "Accept" to "application/json"
  And I include a unique "correlationId"
  And I have a valid access token for scope "<scope>"

- For **UI features**, include:
  Given I launch the application in a supported browser
  And I am on the "<pageName>" page
  And I am logged in as a valid user (if applicable)

- For **mixed** contexts, merge both backgrounds appropriately.

**Scenario / Scenario Outline Sections**
- CRITICAL: Tags MUST appear on a separate line ABOVE the Scenario/Scenario Outline line
- Format must be:
  @tag1 @tag2 @tag3
  Scenario: [scenario name]
  
  OR
  
  @tag1 @tag2 @tag3
  Scenario Outline: [scenario name]

- Must cover: 
  - Positive flows (expected behaviour)
  - Negative flows (invalid inputs, errors)
  - Edge cases (limits, thresholds)
  - Domain-specific exclusions or filters
  - Security, Performance, and Observability (if cues exist)
- Each scenario should have descriptive names.
- Use *Scenario Outline + Examples tables* when parameter variations exist.
- Include *data tables* for field validation (expected JSON fields, required attributes, etc.).
- Tag each scenario with @api, @ui, @security, @performance, @accessibility, or @observability as appropriate.

----------------------------------------------------------------------
### 🧠 Prompt Rules for Output
----------------------------------------------------------------------

1. **Granularity**
   - Produce at least 15 scenarios when requirements are complex (e.g. multi-rule APIs).
   - Split logically across multiple "Feature" files if the text contains functional + non-functional parts.

2. **Coverage Expansion**
   - Identify all explicit and implied rules in the text.
   - For each rule, produce one or more BDD scenarios validating it.
   - For business filters (e.g. "exclude draft claims older than 45 days"), use *Scenario Outline* with positive and negative examples.

3. **Structure Enforcement**
   - Use Background for shared preconditions (headers, auth, correlationId).
   - Include placeholders like `<baseUrl>`, `<policyId>`, `<claimNumber>`, etc.
   - Prefer verbs in step text that map to observable actions ("GET /policies/<policyId>/claims", "response includes", "response excludes").

4. **Tables and Examples**
   - For field validation, use Gherkin tables listing expected properties.
   - For rule variations, use Examples tables with columns for key parameters and expected outcomes (e.g. presence ∈ includes/excludes).

5. **Non-Functional Behaviour**
   - If text mentions response time, availability, or security, include explicit scenarios for:
     - Performance thresholds (`<200 ms for <10 records`)
     - API uptime and health endpoints
     - Security tokens (OAuth2, Basic Auth, JWT validation)
     - TLS enforcement
     - Structured error logging and observability

6. **Error Handling Consistency**
   - Always include a scenario outline covering standard API or UI errors.
   - API: 400, 401, 403, 500, 503
   - UI: Validation, unauthorized access, system failure

7. **Style Constraints**
   - Use concise, clear Gherkin syntax (`Given`, `When`, `Then`, `And`).
   - No prose explanations or commentary outside Gherkin.
   - Avoid low-level implementation details (e.g., payload schema, CSS selectors).
   - Keep scenario steps phrased as observable outcomes.

8. **Field Validation (API)**
   - Represent expected JSON fields in Gherkin tables.
   - Example:
     Then the claim object contains:
       | ClaimNumber | IncidentDate | Status |

9. **UI Behaviour**
   - Use high-level, user-observable actions:
     - "When the user clicks the 'Submit' button"
     - "Then a confirmation message is displayed"
   - Avoid selectors, CSS, or technical IDs.

10. **Performance, Security, Accessibility**
    - If performance cues exist, include measurable thresholds (e.g., response time <200 ms).
    - If security cues exist, include scenarios for OAuth2, JWT, TLS, login/logout, or role-based access.
    - If accessibility cues exist, include scenarios for keyboard navigation, focus order, and screen-reader support.

11. **Observability**
    - When logs or monitoring are mentioned, include a scenario verifying structured logging (JSON with correlationId) or APM alerts.

12. **Formatting & Style**
    - Output *only Gherkin*, no explanations or commentary.
    - Steps must begin with **Given / When / Then / And / But**.
    - Keep language concise, domain-relevant.
    - CRITICAL: Tags MUST be on a separate line above Scenario/Scenario Outline

----------------------------------------------------------------------
### 🧪 Few-Shot Example Pattern (for Model Guidance)
----------------------------------------------------------------------

#### API Example Pattern
@ui @negative
Scenario Outline: Login fails with invalid credentials
  Given I enter "<username>" as the username
  And I enter "<password>" as the password
  When I click the "Login" button
  Then an error message "<errorMessage>" is displayed

  Examples:
    | username      | password       | errorMessage                         |
    | invalidUser   | validPassword  | "Invalid username or password."      |
    | validUsername | wrongPassword  | "Invalid username or password."      |
    |               | validPassword  | "Username is required."              |
    | validUsername |                | "Password is required."              |
    |               |                | "Username and password are required."|

#### UI Example Pattern
@ui @positive
Scenario: Successful login with valid credentials
  Given I enter a valid username "<validUsername>"
  And I enter a valid password "<validPassword>"
  When I click the "Login" button
  Then I am redirected to the "Dashboard" page
  And a welcome message is displayed

#### Mixed Example Pattern
@ui @error @observability
Scenario: System displays generic error for unexpected login failures
  Given an unexpected system error occurs during login
  When I attempt to log in
  Then a generic error message "An unexpected error occurred. Please try again later." is displayed
  And a structured error log is created with a unique correlationId

@ui @performance
Scenario: Login and dashboard pages load within 2 seconds under normal conditions
  Given I am on the "Login" page
  When I enter valid credentials and log in
  Then the "Dashboard" page loads in less than 2 seconds

This pattern demonstrates:
- Tags on separate lines above Scenario/Scenario Outline
- Scenario Outline usage
- Tabular Examples
- Positive/negative cases
- Domain terminology
- Reusable, automation-ready syntax

----------------------------------------------------------------------
### 🧩 Input / Context Placeholders
----------------------------------------------------------------------

Input:
{acceptance_criteria}

Context (Optional):
{context_str}

Tags (Optional):
{tags_str}

----------------------------------------------------------------------
### 🧾 Expected Output Format
----------------------------------------------------------------------

Feature: [Feature title]
[One-line business goal or API behaviour]

  Background:
    Given the API base URL is "<baseUrl>"
    And I set header "Accept" to "application/json"
    And I include a unique "correlationId"
    And I have a valid access token for scope "<scope>"

  @ui @positive
  Scenario: [Positive flow name]
    Given ...
    When ...
    Then ...

  @ui @negative
  Scenario Outline: [Rule or variation name]
    Given ...
    When ...
    Then ...
    Examples:
      | column | column | column | ...

  # Repeat for exclusions, errors, performance, security, observability, etc.
----------------------------------------------------------------------
### ✅ Output Expectations
----------------------------------------------------------------------
- Strict Gherkin syntax (Feature, Background, Scenario, Scenario Outline).
- Multiple features if necessary (Functional, Security, Performance, Observability).
- Each feature includes at least one Background and multiple Scenarios/Outlines.
- Rich Examples and data tables to cover variations.
- Domain-appropriate Backgrounds (API vs UI vs mixed).
- No narrative text outside the Gherkin content.
- Tags MUST appear on a separate line above Scenario/Scenario Outline declarations.
    """
    return prompt

def create_playwright_prompt(test_name: str, test_steps: List[Dict], multiple_test: List[Dict], language: str, page_url: str = None, additional_context: str = None) -> str:
    """Create a detailed prompt for LLM to generate Playwright code"""
    
    prompt = f"""You are an expert test automation engineer. Generate a complete Playwright test in {language} based on the following requirements:

Test Name: {test_name}
Target Language: {language}
{"Base URL: " + page_url if page_url else ""}

Test Steps:
"""
    
    for i, step in enumerate(test_steps, 1):
        action = step.get('action', '')
        selector = step.get('selector', '')
        text = step.get('text', '')
        
        prompt += f"{i}. Action: {action}\n"
        prompt += f"   Selector: {selector}\n"
        if text:
            prompt += f"   Text/Value: {text}\n"
        prompt += "\n"
    
    prompt += "Test Data Variants:\n"
    for i, test_data in enumerate(multiple_test, 1):
        prompt += f"{i}. {test_data.get('title', f'Test {i}')}\n"
        
        for key, value in test_data.items():
            if key != 'title':
                prompt += f"   {key}: {value}\n"
        prompt += "\n"
    
    if additional_context:
        prompt += f"Additional Context: {additional_context}\n\n"
    
    if language.lower() == "javascript":
        prompt += """
Requirements:
- Use @playwright/test framework
- Generate separate test cases for each test data variant
- Use test.describe() to group related tests
- Replace template values (e.g., email/password) with actual values from test data
- Include proper async/await syntax
- Add meaningful assertions and waits where appropriate
- You must not add comments in the code
- Use modern JavaScript best practices
- Use expect() for assertions

Generate ONLY the test code without explanations or markdown formatting.
"""
    elif language.lower() == "java":
        prompt += """
Requirements:
- Use @playwright/test framework
- Generate separate test cases for each test data variant
- Use test.describe() to group related tests
- Replace template values (e.g., email/password) with actual values from test data
- Include proper async/await syntax
- Add meaningful assertions and waits where appropriate
- You must not add comments in the code
- Use modern Java best practices
- normal Java class with main() method, just pure Playwright code that can be run directly.
- Use expect() for assertions

Generate ONLY the test code without explanations or markdown formatting.
"""
    elif language.lower() == "csharp" or language.lower() == "c#":
        prompt += """
Requirements:
- Use Microsoft.Playwright (Version 1.56.0) with NUnit (Version 3.13.3)
- Generate a complete test class with namespace PlaywrightTests.Tests
- Class name should be based on the test name (e.g., {TestName}Tests)
- Include ONLY the following imports:
  * using System;
  * using System.Threading.Tasks;
  * using Microsoft.Playwright;
  * using NUnit.Framework;
  * using PlaywrightTests.Base;

CRITICAL TEST STRUCTURE (follow exactly):
1. Namespace must be: PlaywrightTests.Tests
2. Class must have [TestFixture]
3. Class must inherit from BaseTest:
      public class {TestName}Tests : BaseTest
4. DO NOT create any fields for browser, context, page, or test (these exist in BaseTest)
5. DO NOT generate SetUp / OneTimeSetUp / TearDown methods (BaseTest already handles them)
6. Always access page as Page! (null-forgiving)
7. Always access reporter as Test? (null-conditional)

Test Methods:
- Create separate [Test] methods for each test scenario or dataset
- Signature for each:
      public async Task {MethodName}()
- All test logic must be wrapped in a try-catch:
      try {
          // steps
          Test?.Pass("message");
      }
      catch (Exception ex) {
          Test?.Fail($"Exception occurred: {ex.Message}");
          throw;
      }
- Replace template values with actual test data
- All Playwright calls must use await
- Add real assertions using:
      Assert.That(...)
      await Assertions.Expect(...).ToHaveTextAsync(...)
  (Use ONLY this Playwright assertion style for compatibility with v1.56.0)

Playwright Assertion Rules (IMPORTANT):
- Use NUnit assertion:
      Assert.That(value, Is.EqualTo("expected"));
- Use Playwright 1.56 assertion:
      await Assertions.Expect(locator).ToHaveTextAsync("expected");
- Example combined validation:
      Assert.That(linkText, Is.EqualTo("Dashboard"));
      await Assertions.Expect(dashboardLink).ToHaveTextAsync("Dashboard");
      Assert.That(dashboardText.Trim(), Is.EqualTo("Dashboard"), 
          "Dashboard link text should match expected value.");

Example structure to follow:
using System;
using System.Threading.Tasks;
using Microsoft.Playwright;
using NUnit.Framework;
using PlaywrightTests.Base;

namespace PlaywrightTests.Tests
{
    [TestFixture]
    public class LoginTests : BaseTest
    {
        [Test]
        public async Task LoginWithValidCredentials()
        {
            try
            {
                await Page!.GotoAsync("https://example.com/login");
                await Page!.FillAsync("//input[@id='email']", "user@example.com");
                await Page!.FillAsync("//input[@id='password']", "password123");
                await Page!.ClickAsync("//button[@type='submit']");
                Test?.Pass("Login successful");
            }
            catch (Exception ex)
            {
                Test?.Fail($"Exception occurred: {ex.Message}");
                throw;
            }
        }
    }
}

IMPORTANT OUTPUT RULES:
- Generate ONLY the test class (no explanations, no markdown formatting)
- No comments unless absolutely required
- No SetUp/TearDown overrides
- No browser/context/page/test declarations
- Must use Page! and Test?
- Must use NUnit Assert.That and Playwright Assertions.Expect for all validations

Generate ONLY the test class code without explanations or markdown formatting.
"""
    else:  # Python
        prompt += """
Requirements:
- Use pytest with playwright.sync_api
- Generate separate test functions for each test data variant
- Include proper imports (pytest, Page, expect)
- Replace template values (e.g., email/password) with actual values from test data
- Add meaningful assertions and waits where appropriate
- Use Python best practices.
- You must not add docstrings and comments in the code.
- Use expect() for assertions.
- Follow PEP 8 style guidelines.

Generate ONLY the test code without explanations or markdown formatting.
"""
    
    return prompt

def parse_bdd_response(raw_response: str) -> Dict:
    """
    Process the AI-generated response into structured BDD format
    Fixed version with proper null checks and error handling
    """
    lines = raw_response.strip().split("\n")
    
    feature = ""
    scenarios = []
    current_scenario = None
    current_step_type = None
    
    for line in lines:
        line = line.strip()
        if not line:
            continue
        
        # Skip lines that are comments or markdown formatting
        if line.startswith("#") or line.startswith("```"):
            continue
            
        if line.lower().startswith("feature:"):
            feature = line[8:].strip()
        elif line.lower().startswith("scenario outline:"):
            # Save previous scenario if exists
            if current_scenario:
                scenarios.append(current_scenario)
            current_scenario = {
                "name": line[17:].strip(),
                "given": [],
                "when": [],
                "then": []
            }
            current_step_type = None
        elif line.lower().startswith("scenario:"):
            # Save previous scenario if exists
            if current_scenario:
                scenarios.append(current_scenario)
            current_scenario = {
                "name": line[9:].strip(),
                "given": [],
                "when": [],
                "then": []
            }
            current_step_type = None
        elif line.lower().startswith("background:"):
            # Skip background section for now
            current_step_type = "skip"
            continue
        elif line.lower().startswith("examples:"):
            # Skip examples section
            current_step_type = "skip"
            continue
        elif line.startswith("|"):
            # Skip table rows in examples
            continue
        elif current_scenario is not None:
            # Only process steps if we have a valid scenario
            if line.lower().startswith("given "):
                current_step_type = "given"
                current_scenario["given"].append(line[6:].strip())
            elif line.lower().startswith("when "):
                current_step_type = "when"
                current_scenario["when"].append(line[5:].strip())
            elif line.lower().startswith("then "):
                current_step_type = "then"
                current_scenario["then"].append(line[5:].strip())
            elif line.lower().startswith("and ") and current_step_type and current_step_type != "skip":
                step_text = line[4:].strip()
                if current_step_type in current_scenario:
                    current_scenario[current_step_type].append(f"And {step_text}")
            elif line.lower().startswith("but ") and current_step_type and current_step_type != "skip":
                step_text = line[4:].strip()
                if current_step_type in current_scenario:
                    current_scenario[current_step_type].append(f"But {step_text}")
    
    # Add the last scenario if it exists
    if current_scenario:
        scenarios.append(current_scenario)
    
    # If no feature name found, use a default
    if not feature:
        feature = "Generated Test Feature"
    
    # If no scenarios found, create a default one to prevent errors
    if not scenarios:
        scenarios = [{
            "name": "Default Scenario",
            "given": ["the application is running"],
            "when": ["I perform an action"],
            "then": ["I should see the expected result"]
        }]
    
    return {
        "feature": feature,
        "scenarios": scenarios
    }

def generate_test_code(structured_bdd: Dict, language: str = "c#") -> str:
    """
    Convert structured BDD into executable test code
    """
    if language == "c#" or language == "C#":
        return generate_nunit_bdd_code(structured_bdd)
    elif language == "python" or language == "Python":
        return generate_pytest_bdd_code(structured_bdd)
    elif language == "java" or language == "Java":
        return generate_cucumber_java_code(structured_bdd)
    elif language == "javascript" or language == "JavaScript":
        return generate_cucumber_js_code(structured_bdd)
    else:
        raise ValueError(f"Unsupported language: {language}")


def generate_pytest_bdd_code(bdd: Dict) -> str:
    """Generate pytest-bdd compatible Python code"""
    feature_name = bdd["feature"]
    scenarios = bdd["scenarios"]
    
    code = f"""
from pytest_bdd import scenario, given, when, then, parsers
from pytest import fixture

# Feature: {feature_name}
"""

    processed_steps = set()
    
    for i, scenario in enumerate(scenarios):
        scenario_name = scenario["name"]
        function_name = scenario_name.lower().replace(" ", "_").replace("-", "_")
        
        code += f"""
@scenario('test_{function_name}.feature', '{scenario_name}')
def test_{function_name}():
    \"\"\"Test for: {scenario_name}\"\"\"
    pass

"""
        for given_step in scenario["given"]:
            clean_step = given_step
            if given_step.startswith("And "):
                clean_step = given_step[4:]
            elif given_step.startswith("But "):
                clean_step = given_step[4:]
            
            if clean_step not in processed_steps:
                method_name = clean_step[:20].lower().replace(" ", "_").replace("'", "")
                method_name = ''.join(c for c in method_name if c.isalnum() or c == '_')
                
                code += f"""
@given(parsers.parse('{clean_step}'))
def given_{method_name}(context):
    # TODO: Implement Given step
    pass
"""
                processed_steps.add(clean_step)

        for when_step in scenario["when"]:
            clean_step = when_step
            if when_step.startswith("And "):
                clean_step = when_step[4:]
            elif when_step.startswith("But "):
                clean_step = when_step[4:]
            
            if clean_step not in processed_steps:
                method_name = clean_step[:20].lower().replace(" ", "_").replace("'", "")
                method_name = ''.join(c for c in method_name if c.isalnum() or c == '_')
                
                code += f"""
@when(parsers.parse('{clean_step}'))
def when_{method_name}(context):
    # TODO: Implement When step
    pass
"""
                processed_steps.add(clean_step)

        for then_step in scenario["then"]:
            clean_step = then_step
            if then_step.startswith("And "):
                clean_step = then_step[4:]
            elif then_step.startswith("But "):
                clean_step = then_step[4:]
            
            if clean_step not in processed_steps:
                method_name = clean_step[:20].lower().replace(" ", "_").replace("'", "")
                method_name = ''.join(c for c in method_name if c.isalnum() or c == '_')
                
                code += f"""
@then(parsers.parse('{clean_step}'))
def then_{method_name}(context):
    # TODO: Implement Then step
    pass
"""
                processed_steps.add(clean_step)
    
    return code


def generate_cucumber_java_code(bdd: Dict) -> str:
    """Generate Cucumber Java code"""
    feature_name = bdd["feature"]
    scenarios = bdd["scenarios"]
    
    class_name = ''.join(word.capitalize() for word in feature_name.split())
    if not class_name.endswith("StepDefs"):
        class_name += "StepDefs"
    
    code = f"""package stepDefinitions;

import io.cucumber.java.en.Given;
import io.cucumber.java.en.When;
import io.cucumber.java.en.Then;
import org.junit.Assert;

public class {class_name} {{

"""
    
    processed_steps = set()
    
    for scenario in scenarios:
        for given_step in scenario["given"]:
            clean_step = given_step
            if given_step.startswith("And "):
                clean_step = given_step[4:]
            elif given_step.startswith("But "):
                clean_step = given_step[4:]
            
            if clean_step not in processed_steps:
                method_name = clean_step[:30].lower().replace(" ", "_").replace("'", "").replace("\"", "")
                method_name = ''.join(c for c in method_name if c.isalnum() or c == '_')
                
                step_text = clean_step.replace("\"", "\\\"").replace("\\", "\\\\")
                
                code += f"""    @Given("{step_text}")
    public void given_{method_name}() {{
        // TODO: Implement Given step
        throw new io.cucumber.java.PendingException();
    }}

"""
                processed_steps.add(clean_step)

        for when_step in scenario["when"]:
            clean_step = when_step
            if when_step.startswith("And "):
                clean_step = when_step[4:]
            elif when_step.startswith("But "):
                clean_step = when_step[4:]
            
            if clean_step not in processed_steps:
                method_name = clean_step[:30].lower().replace(" ", "_").replace("'", "").replace("\"", "")
                method_name = ''.join(c for c in method_name if c.isalnum() or c == '_')
                
                step_text = clean_step.replace("\"", "\\\"").replace("\\", "\\\\")
                
                code += f"""    @When("{step_text}")
    public void when_{method_name}() {{
        // TODO: Implement When step
        throw new io.cucumber.java.PendingException();
    }}

"""
                processed_steps.add(clean_step)

        for then_step in scenario["then"]:
            clean_step = then_step
            if then_step.startswith("And "):
                clean_step = then_step[4:]
            elif then_step.startswith("But "):
                clean_step = then_step[4:]
            
            if clean_step not in processed_steps:
                method_name = clean_step[:30].lower().replace(" ", "_").replace("'", "").replace("\"", "")
                method_name = ''.join(c for c in method_name if c.isalnum() or c == '_')
                
                step_text = clean_step.replace("\"", "\\\"").replace("\\", "\\\\")
                
                code += f"""    @Then("{step_text}")
    public void then_{method_name}() {{
        // TODO: Implement Then step
        Assert.fail("Step not yet implemented");
    }}

"""
                processed_steps.add(clean_step)
    
    code += "}"
    
    return code


def generate_cucumber_js_code(bdd: Dict) -> str:
    """Generate Cucumber JavaScript code"""
    feature_name = bdd["feature"]
    scenarios = bdd["scenarios"]
    
    code = f"""const {{ Given, When, Then }} = require('@cucumber/cucumber');
const {{ expect }} = require('chai');

// Feature: {feature_name}

"""
    
    processed_steps = set()
    
    for scenario in scenarios:
        for given_step in scenario["given"]:
            clean_step = given_step
            if given_step.startswith("And "):
                clean_step = given_step[4:]
            elif given_step.startswith("But "):
                clean_step = given_step[4:]
            
            if clean_step not in processed_steps:
                step_text = clean_step.replace("'", "\\'").replace("\"", "\\\"")
                
                code += f"""Given('{step_text}', function () {{
    // TODO: Implement Given step
    return 'pending';
}});

"""
                processed_steps.add(clean_step)

        for when_step in scenario["when"]:
            clean_step = when_step
            if when_step.startswith("And "):
                clean_step = when_step[4:]
            elif when_step.startswith("But "):
                clean_step = when_step[4:]
            
            if clean_step not in processed_steps:
                step_text = clean_step.replace("'", "\\'").replace("\"", "\\\"")
                
                code += f"""When('{step_text}', function () {{
    // TODO: Implement When step
    return 'pending';
}});

"""
                processed_steps.add(clean_step)

        for then_step in scenario["then"]:
            clean_step = then_step
            if then_step.startswith("And "):
                clean_step = then_step[4:]
            elif then_step.startswith("But "):
                clean_step = then_step[4:]
            
            if clean_step not in processed_steps:
                step_text = clean_step.replace("'", "\\'").replace("\"", "\\\"")
                
                code += f"""Then('{step_text}', function () {{
    // TODO: Implement Then step
    throw new Error('Step not yet implemented');
}});

"""
                processed_steps.add(clean_step)
    
    return code

def generate_nunit_bdd_code(bdd: Dict) -> str:
    """Generate NUnit and SpecFlow compatible C# code"""
    feature_name = bdd["feature"]
    scenarios = bdd["scenarios"]
    
    class_name = ''.join(word.capitalize() for word in feature_name.split())
    if not class_name.endswith("Steps"):
        class_name += "Steps"
    
    code = f"""using NUnit.Framework;
using TechTalk.SpecFlow;
using System;
using System.Threading.Tasks;

namespace TestAutomation.StepDefinitions
{{
    [Binding]
    public class {class_name}
    {{
        private readonly ScenarioContext _scenarioContext;

        public {class_name}(ScenarioContext scenarioContext)
        {{
            _scenarioContext = scenarioContext;
        }}

"""
    
    processed_steps = set()
    
    for i, scenario in enumerate(scenarios):
        scenario_name = scenario["name"]
        function_name = ''.join(word.capitalize() for word in scenario_name.split())
        function_name = ''.join(c for c in function_name if c.isalnum())
        
        code += f"""        [Test]
        [Scenario("{scenario_name}")]
        public void Test_{function_name}()
        {{
            // This method ties the test to the corresponding scenario in the feature file
            // Steps are implemented separately below
        }}

"""
        
        for given_step in scenario["given"]:
            clean_step = given_step
            if given_step.startswith("And "):
                clean_step = given_step[4:]
            elif given_step.startswith("But "):
                clean_step = given_step[4:]
            
            if clean_step not in processed_steps:
                method_name = clean_step[:20].lower().replace(" ", "_").replace("'", "")
                method_name = ''.join(c for c in method_name if c.isalnum() or c == '_')
                
                step_text = clean_step.replace("\"", "\\\"")
                
                code += f"""        [Given(@"{step_text}")]
        public void Given_{method_name}()
        {{
            // TODO: Implement Given step
            throw new PendingStepException();
        }}

"""
                processed_steps.add(clean_step)

        for when_step in scenario["when"]:
            clean_step = when_step
            if when_step.startswith("And "):
                clean_step = when_step[4:]
            elif when_step.startswith("But "):
                clean_step = when_step[4:]
            
            if clean_step not in processed_steps:
                method_name = clean_step[:20].lower().replace(" ", "_").replace("'", "")
                method_name = ''.join(c for c in method_name if c.isalnum() or c == '_')
                
                step_text = clean_step.replace("\"", "\\\"")
                
                code += f"""        [When(@"{step_text}")]
        public void When_{method_name}()
        {{
            // TODO: Implement When step
            throw new PendingStepException();
        }}

"""
                processed_steps.add(clean_step)

        for then_step in scenario["then"]:
            clean_step = then_step
            if then_step.startswith("And "):
                clean_step = then_step[4:]
            elif then_step.startswith("But "):
                clean_step = then_step[4:]
            
            if clean_step not in processed_steps:
                method_name = clean_step[:20].lower().replace(" ", "_").replace("'", "")
                method_name = ''.join(c for c in method_name if c.isalnum() or c == '_')
                
                step_text = clean_step.replace("\"", "\\\"")
                
                code += f"""        [Then(@"{step_text}")]
        public void Then_{method_name}()
        {{
            // TODO: Implement Then step
            Assert.fail("Step not yet implemented");
        }}

"""
                processed_steps.add(clean_step)
    
    code += """    }
}"""
    
    return code

# --- API Endpoints ---

@app.post("/api/test-cases/generate")
async def generate_test_case(request: TestCaseGenerationRequest):
    """
    API endpoint for generating test cases from acceptance criteria
    """
    try:

        if request.bdd is not None:
            try:
                if not request.bdd.get("scenarios"):
                    raise HTTPException(
                        status_code=400, 
                        detail="Invalid BDD structure: missing scenarios"
                    )
                
                code = generate_test_code(request.bdd, request.language)
                
                return {
                    "code": code,
                    "language": request.language,
                    "feature": request.bdd.get("feature", ""),
                    "scenario_count": len(request.bdd.get("scenarios", []))
                }
                
            except ValueError as e:
                raise HTTPException(status_code=400, detail=str(e))
            except Exception as e:
                logger.error(f"Error generating code from BDD: {str(e)}")
                raise HTTPException(status_code=500, detail=f"Error generating code from BDD: {str(e)}")

        acceptance_criteria = request.acceptance_criteria.strip() if request.acceptance_criteria else None
        if not acceptance_criteria:
            acceptance_criteria = None
        
        prompt = generate_test_case_prompt(acceptance_criteria, request.project_context, request.tags)
        print(prompt)
        try:
            response = client.chat.completions.create(
                model=LLM_MODEL,
                messages=[
                    {"role": "system", "content": "You are a senior QA automation engineer embedded in a cross-functional Agile team. Your task is to convert business requirements—provided as user stories, acceptance criteria, or product descriptions—into structured BDD-style test scenarios."},
                    {"role": "user", "content": prompt}
                ],
                temperature=0.5,
                max_tokens=2000
            )
        except Exception as ai_error:
            logger.error(f"AI API Error: {str(ai_error)}")
            raise HTTPException(
                status_code=503, 
                detail="Something went wrong while contacting the AI engine. Please try again in a few moments."
            )
        
        raw_response = response.choices[0].message.content
        
        structured_bdd = parse_bdd_response(raw_response)

        try:
            validate_bdd_output(structured_bdd)
        except InsufficientContentError as e:
            raise HTTPException(
                status_code=422,
                detail={
                    "message": "We couldn't generate usable test scenarios from the provided description. Please try refining your input or add more context.",
                    "error_type": "insufficient_content",
                    "can_retry": True,
                    "original_input": request.acceptance_criteria
                }
            )

        code = generate_test_code(structured_bdd, request.language)
        
        bdd_preview = format_bdd_for_preview(structured_bdd)
        
        return {
            "bdd": structured_bdd,  # This was the parsed structure with array of scenarios
            "preview": raw_response, #bdd_preview, --old one
            "code": code,
            # "raw_response": raw_response
        }
        # return {
        #     "bdd": {
        #         "feature": structured_bdd.get("feature", ""),
        #         "scenarios": raw_response  # Now contains the raw Gherkin text
        #     },
        #     "preview": raw_response,
        #     "code": code,
        #         }
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Error generating test case: {str(e)}")
        if "no scenarios" in str(e).lower() or "empty" in str(e).lower():
            raise HTTPException(
                status_code=422,
                detail={
                    "message": "We couldn't generate test scenarios from the provided description. Please try refining your input or add more context.",
                    "error_type": "insufficient_content", 
                    "can_retry": True,
                    "original_input": request.acceptance_criteria
                }
            )
        raise HTTPException(status_code=500, detail=f"Error generating test case: {str(e)}")


def format_bdd_for_preview(bdd: Dict) -> str:
    """Format BDD structure for preview display"""
    feature = bdd["feature"]
    scenarios = bdd["scenarios"]
    
    preview = f"Feature: {feature}\n\n"
    
    for scenario in scenarios:
        preview += f"  Scenario: {scenario['name']}\n"
        
        for i, given in enumerate(scenario["given"]):
            if i == 0:
                if given.lower().startswith("given "):
                    preview += f"    {given}\n"
                else:
                    preview += f"    Given {given}\n"
            else:
                if given.lower().startswith(("and ", "but ")):
                    preview += f"    {given}\n"
                else:
                    preview += f"    And {given}\n"
        
        for i, when in enumerate(scenario["when"]):
            if i == 0:
                if when.lower().startswith("when "):
                    preview += f"    {when}\n"
                else:
                    preview += f"    When {when}\n"
            else:
                if when.lower().startswith(("and ", "but ")):
                    preview += f"    {when}\n"
                else:
                    preview += f"    And {when}\n"
        
        for i, then in enumerate(scenario["then"]):
            if i == 0:
                if then.lower().startswith("then "):
                    preview += f"    {then}\n"
                else:
                    preview += f"    Then {then}\n"
            else:
                if then.lower().startswith(("and ", "but ")):
                    preview += f"    {then}\n"
                else:
                    if any(neg_word in then.lower() for neg_word in ['not', 'should not', 'cannot', 'unable', 'fail', 'error', 'invalid']):
                        preview += f"    But {then}\n"
                    else:
                        preview += f"    And {then}\n"
            
        preview += "\n"
        
    return preview


@app.post("/api/playwright/generate-llm")
async def generate_playwright_test_with_llm(request: PlaywrightGenerationRequest):
    """
    API endpoint for generating Playwright test code using LLM
    """
    try:
        if not request.test_steps:
            raise HTTPException(
                status_code=400, 
                detail="No test steps provided"
            )
        
        if request.language.lower() not in ["javascript", "python", "java", "csharp", "c#"]:
            raise HTTPException(
                status_code=400,
                detail="Language must be 'javascript', 'python', 'java', or 'csharp'"
            )
        
        multiple_test_dict = []
        for test in request.multipleTest:
            test_dict = test.model_dump()
            multiple_test_dict.append(test_dict)
            
        prompt = create_playwright_prompt(
            test_name=request.test_name,
            test_steps=request.test_steps,
            multiple_test=multiple_test_dict,
            language=request.language,
            page_url=request.page_url,
            additional_context=request.additional_context
        )
        
        logger.info(f"Generating {request.language} code for test: {request.test_name} with {len(request.multipleTest)} variants")
        print("\n================================\n",request.language," Prompt:\n", prompt)

        try:
            response = client.chat.completions.create(
                model="gpt-4.1",
                messages=[
                    {
                        "role": "system", 
                        "content": "You are an expert test automation engineer specializing in Playwright. Generate clean, production-ready test code."
                    },
                    {
                        "role": "user", 
                        "content": prompt
                    }
                ],
                max_tokens=3000,
                temperature=0.1,
                top_p=0.9
            )
        
            generated_code = response.choices[0].message.content.strip()
        
        except Exception as e:
            logger.error(f"Error calling OpenAI API: {str(e)}")
            raise HTTPException(status_code=500, detail=f"LLM generation failed: {str(e)}")
        
        if not generated_code:
            raise HTTPException(
                status_code=500,
                detail="LLM returned empty code"
            )
        
        return {
            "code": generated_code,
            "language": request.language,
            "test_name": request.test_name,
            "step_count": len(request.test_steps),
            "variant_count": len(request.multipleTest)
        }
        
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Error generating Playwright code with LLM: {str(e)}")
        raise HTTPException(
            status_code=500, 
            detail=f"Error generating Playwright code: {str(e)}"
        )


@app.post("/api/refine-acceptance-criteria")
async def refine_acceptance_criteria(request: dict):
    """
    Refines vague or incomplete acceptance criteria using an LLM.
    """
    try:
        user_input = request.get("input", "").strip()
        
        if not user_input:
            return {"refined": "Please provide some initial acceptance criteria or a brief description of the feature."}
        
        refinement_prompt = f"""
        The user provided the following vague or incomplete acceptance criteria: "{user_input}"

        Rewrite this into a clear and complete acceptance criterion suitable for generating test cases.
        Be specific about:
        - The user action or system behavior
        - Expected outcomes
        - Preconditions if needed

        Example Input: "test login"
        Example Output: "Users should be able to log in with a valid email and password and should be redirected to the dashboard on successful login."

        Return only the rewritten acceptance criteria as a plain string.
        """

        response = client.chat.completions.create(
            model=LLM_MODEL,
            messages=[
                {"role": "system", "content": "You are a test analyst helping to rewrite vague acceptance criteria into clear, testable statements."},
                {"role": "user", "content": refinement_prompt}
            ],
            temperature=0.4,
            max_tokens=200
        )

        refined_text = response.choices[0].message.content.strip().strip('"')
        return {"refined": refined_text}
    
    except Exception as e:
        logger.error(f"Error refining acceptance criteria: {str(e)}")
        return {"refined": "Unable to process input. Please try again later or provide a more descriptive statement."}


if __name__ == "__main__":
    import uvicorn
    
    uvicorn.run(app, host=os.getenv("UVICORN_HOST"), port=int(os.getenv("UVICORN_PORT")))

# Modified function to create Playwright prompt on 04/11/2025
