# rssi_power_module.py
# Versão: 4.8 (Build com Edição In-line)
# Data: 01/09/2025
# Descrição: Módulo de análise de RSSI vs Potência com sistema de histórico de testes
#            - NOVO: Edição in-line do nome do teste no histórico com duplo-clique.
#            - MELHORIA: Teste recém-concluído agora permanece plotado e marcado no histórico.
#            - NOVO: Tooltip interativo ao passar o mouse sobre os pontos do gráfico.
#            - CORREÇÃO: Tooltip agora funciona corretamente após atualizações do gráfico.
#            - NOVO: Controle de ordenação em todos os headers da tabela de histórico.
#            - CORREÇÃO: EPC completo agora é exibido na tabela de histórico e nos nomes dos gráficos.
#            - CORREÇÃO: Ordenação das colunas Potência e RSSI agora funciona corretamente.

import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import queue
import threading
import time
import os
import ctypes
import struct
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import matplotlib
from src.core.port_manager import get_port_manager
import json
from datetime import datetime
from .i18n import get_translator, t

try:
    matplotlib.use('TkAgg')
except Exception:
    matplotlib.use('Agg')

try:
    from .rssi_power_database import RSSIPowerDatabase
    DATABASE_AVAILABLE = True
except ImportError:
    try:
        from rssi_power_database import RSSIPowerDatabase
        DATABASE_AVAILABLE = True
    except ImportError:
        DATABASE_AVAILABLE = False

DLL_NAME = "UHFRFID.dll"
try:
    dll_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), DLL_NAME)
    rfid_sdk = ctypes.CDLL(dll_path)
    rfid_sdk.UHF_RFID_Open.argtypes = [ctypes.c_ubyte, ctypes.c_int]; rfid_sdk.UHF_RFID_Open.restype = ctypes.c_int
    rfid_sdk.UHF_RFID_Close.argtypes = [ctypes.c_ubyte]; rfid_sdk.UHF_RFID_Close.restype = ctypes.c_int
    rfid_sdk.UHF_RFID_Set.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_uint, ctypes.c_char_p, ctypes.POINTER(ctypes.c_uint)]; rfid_sdk.UHF_RFID_Set.restype = ctypes.c_int
    HARDWARE_AVAILABLE = True
except (OSError, AttributeError):
    rfid_sdk = None
    HARDWARE_AVAILABLE = False

# --- Constantes dos Comandos ---
# COM_PORT = 4 ### ALTERADO: Removido
BAUD_RATE = 115200
RFID_CMD_SET_TXPOWER = 0x10
RFID_CMD_SET_FREQ_TABLE = 0x14
RFID_CMD_INV_TAG = 0x80
RFID_CMD_STOP_INVENTORY = 0x8C
RFID_CMD_SET_CW_STATUS = 0x24
RFID_CMD_GET_TEMPERATURE = 0x34
TEMPERATURE_LIMIT = 50.0

class TagInfo:
    def __init__(self, epc, rssi): self.epc = epc; self.rssi = rssi
    def __repr__(self): return f"Tag(EPC={self.epc}, RSSI={self.rssi}dBm)"

class RSSIPowerModule(tk.Frame):
    ### ALTERADO: Adicionado com_port=4 ###
    def __init__(self, parent, app_shell=None, demo_mode=False, com_port=4):
        super().__init__(parent)
        self.parent = parent
        self.app_shell = app_shell
        self.demo_mode = demo_mode
        self.com_port = com_port ### NOVO: Armazena a porta COM ###
        self.com_port_lock = threading.Lock()
        self.port_manager = get_port_manager()
        self.translator = get_translator()
        self._widget_refs = {}  # Para armazenar referências aos widgets para atualização de idioma

        # Inicializa limites da licença com valores padrão (modo browser)
        self.license_limits = {'min_freq': 800, 'max_freq': 1000, 'min_power': 5, 'max_power': 25, 'is_licensed': False}
        
        # Atualiza limites da licença se app_shell estiver disponível
        if app_shell and hasattr(app_shell, 'license_limits'):
            self.license_limits = app_shell.license_limits
        elif app_shell and hasattr(app_shell, '_calculate_license_limits'):
            # Tenta obter limites da licença ativa
            try:
                valid_lics = ["RSSIPower", "FastChecker"]
                license_limits = app_shell._calculate_license_limits(valid_lics)
                if license_limits.get('is_licensed', False):
                    self.license_limits = license_limits
                else:
                    pass
            except Exception as e:
                pass
        
        self.test_running = False
        self.sweep_thread = None
        self.data_queue = queue.Queue()
        
        self.live_power_data = []
        self.live_rssi_data = []
        self.current_power = None  # Potência atual para desenhar a linha progressiva
        
        self.historical_plots = {}
        self.multiple_tests = {}  # NOVO: Para persistir dados e cores dos testes
        self.test_colors = {}     # NOVO: Para persistir cores por ID do teste
        
        self.available_tags = []
        self.selected_tag_info = None
        
        self.test_name_var = tk.StringVar(value="Teste_RSSI")
        self.current_test_name = None  # NOVO: Nome do teste atual para persistência
        self.freq_var = tk.DoubleVar(value=915.0)
        self.samples_var = tk.IntVar(value=3)
        self.power_step_var = tk.DoubleVar(value=0.5)
        
        # NOVO: Controle de ordenação da tabela de histórico
        self.current_sort_column = None
        self.current_sort_reverse = False
        
        self.database = None
        # Persistência simples de configurações do módulo
        self.settings_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), "rssi_power_settings.json")
        if DATABASE_AVAILABLE:
            try:
                self.database = RSSIPowerDatabase()
            except Exception as e:
                pass
        
        self.setup_ui()
        # Carrega configurações persistidas após criar a UI
        try:
            self._load_persistent_settings()
        except Exception:
            pass
        
        if not HARDWARE_AVAILABLE:
            messagebox.showerror(t('rssi_power.dll_error'), t('rssi_power.dll_error_msg').format(dll=DLL_NAME))
            for btn in [self.scan_button, self.start_test_button]: btn.config(state="disabled")

        self.process_data_queue()
        
        # REQUISITOS: Captura porta + Reset ao entrar
        self._initialize_hardware_on_enter()

    def _initialize_hardware_on_enter(self):
        """REQUISITOS 1+4: Captura porta e reseta hardware"""
        if not HARDWARE_AVAILABLE or not rfid_sdk:
            return
        
        try:
            print(f"🔧 RSSI x Power: Inicializando hardware...")
            
            if self.port_manager.acquire_port("RSSIPower", timeout=2.0):
                try:
                    if rfid_sdk.UHF_RFID_Open(self.com_port, BAUD_RATE) == 0:
                        print(f"✅ RSSI x Power: Porta COM{self.com_port} capturada")
                        
                        out_buf = ctypes.create_string_buffer(64)
                        out_len = ctypes.c_uint(0)
                        rfid_sdk.UHF_RFID_Set(RFID_CMD_STOP_INVENTORY, None, 0, out_buf, ctypes.byref(out_len))
                        
                        rfid_sdk.UHF_RFID_Close(self.com_port)
                        print(f"✅ RSSI x Power: Hardware inicializado (sem bip)")
                finally:
                    self.port_manager.release_port("RSSIPower")
        except Exception as e:
            print(f"⚠️ RSSI x Power: Erro: {e}")
    
    def get_temperature(self):
        """Lê a temperatura do hardware RFID"""
        if not HARDWARE_AVAILABLE or not rfid_sdk:
            return None
        try:
            output_buffer = ctypes.create_string_buffer(64)
            output_len = ctypes.c_uint(0)
            status = rfid_sdk.UHF_RFID_Set(RFID_CMD_GET_TEMPERATURE, None, 0, output_buffer, ctypes.byref(output_len))
            if status == 0 and output_len.value >= 3:
                temp_val = struct.unpack('>h', output_buffer.raw[1:3])[0]
                return temp_val / 100.0
        except Exception as e:
            print(f"⚠️ Erro ao ler temperatura: {e}")
        return None

    def destroy(self):
        """REQUISITOS 2+3+4: Libera porta, para comandos, reseta"""
        try:
            print(f"🔄 RSSI x Power: Iniciando cleanup...")
            # Salva configurações persistentes ao sair
            try:
                self._save_persistent_settings()
            except Exception:
                pass
            
            if hasattr(self, 'test_running') and self.test_running:
                self.test_running = False
            
            if HARDWARE_AVAILABLE and rfid_sdk:
                try:
                    if self.port_manager.acquire_port("RSSIPower", timeout=2.0):
                        try:
                            if rfid_sdk.UHF_RFID_Open(self.com_port, BAUD_RATE) == 0:
                                out_buf = ctypes.create_string_buffer(64)
                                out_len = ctypes.c_uint(0)
                                rfid_sdk.UHF_RFID_Set(RFID_CMD_STOP_INVENTORY, None, 0, out_buf, ctypes.byref(out_len))
                            rfid_sdk.UHF_RFID_Close(self.com_port)
                            print(f"✅ RSSI x Power: Comandos parados, porta liberada (sem bip)")
                        finally:
                            self.port_manager.release_port("RSSIPower")
                except Exception as e:
                    print(f"⚠️ RSSI x Power: Erro: {e}")
            
            print(f"✅ RSSI x Power: Cleanup concluído")
        except:
            pass
        
        try:
            super().destroy()
        except:
            pass
    
    def update_license_status(self, new_license_status):
        """NOVO: Atualiza o status da licença e a interface do usuário"""
        try:
            print(f"🔔 RSSI x Power: Atualizando status da licença para {new_license_status}")
            
            # Atualiza o status interno
            if new_license_status:
                # Com licença: obtém limites do app_shell
                if hasattr(self, 'app_shell') and self.app_shell:
                    self._update_license_limits_from_app_shell()
                else:
                    # Fallback: licença padrão
                    self.license_limits = {
                        'min_freq': 800, 'max_freq': 1000,
                        'min_power': 5, 'max_power': 25,
                        'is_licensed': True
                    }
            else:
                # Sem licença: modo browser
                self.license_limits = {
                    'min_freq': 800, 'max_freq': 1000,
                    'min_power': 5, 'max_power': 25,
                    'is_licensed': False
                }
            
            # Atualiza a interface
            self.update_ui_state()
            
            
        except Exception as e:
            pass
    
    def _update_license_limits_from_app_shell(self):
        """Atualiza limites de licença a partir do app_shell"""
        try:
            if not hasattr(self, 'app_shell') or self.app_shell is None:
                return
            
            valid_lics = ["RSSIPower", "FastChecker"]
            license_limits = self.app_shell._calculate_license_limits(valid_lics)
            
            if license_limits.get('is_licensed', False):
                self.license_limits = license_limits
                self._update_frequency_spinbox()
                self.update_ui_state()
            else:
                self.license_limits = {'min_freq': 800, 'max_freq': 1000, 'min_power': 5, 'max_power': 25, 'is_licensed': False}
                self.update_ui_state()
            
        except Exception as e:
            self.license_limits = {'min_freq': 800, 'max_freq': 1000, 'min_power': 5, 'max_power': 25, 'is_licensed': False}
            self.update_ui_state()
    
    def _update_frequency_spinbox(self):
        """Atualiza os limites do spinbox de frequência baseado na licença"""
        try:
            if hasattr(self, 'freq_spinbox'):
                min_freq = self.license_limits.get('min_freq', 800)
                max_freq = self.license_limits.get('max_freq', 1000)
                self.freq_spinbox.config(from_=min_freq, to=max_freq)
                pass
        except Exception as e:
            pass
    
    # ... (O resto da classe é muito grande, mas a lógica de substituição é a mesma)
    # Vou aplicar as alterações nos métodos relevantes.
    
    def power_sweep_worker(self):
        try:
            with self.com_port_lock:
                ### ALTERADO: usa self.com_port ###
                if not self.port_manager.acquire_port("RSSIPower", timeout=2.0):
                    self.data_queue.put({'type': 'error', 'error': f'Timeout ao adquirir COM{self.com_port}'}); return
                if rfid_sdk.UHF_RFID_Open(self.com_port, BAUD_RATE) != 0:
                    self.data_queue.put({'type': 'error', 'error': f'Falha ao conectar ao hardware na COM{self.com_port}'}); return
                try:
                    if not self._set_frequency(self.freq_var.get()):
                        self.data_queue.put({'type': 'error', 'error': 'Falha ao configurar frequência'}); return
                    min_power, max_power = self.license_limits['min_power'], self.license_limits['max_power']
                    current_power = min_power
                    while current_power <= max_power and self.test_running:
                        self._set_power(current_power)
                        # Envia a potência atual para mostrar progresso ANTES de medir (para visualização inicial)
                        self.data_queue.put({'type': 'current_power', 'power': current_power})
                        rssi_values = []
                        for _ in range(self.samples_var.get()):
                            if not self.test_running: break
                            tags = self._scan_for_tags_internal(duration_sec=0.1)
                            for tag in tags:
                                if tag.epc == self.selected_tag_info.epc: rssi_values.append(tag.rssi)
                            time.sleep(0.01)
                        if rssi_values:
                            # Envia o ponto plotado DEPOIS da medição - isso vai sincronizar a barra com o ponto
                            self.data_queue.put({'type': 'update', 'power': current_power, 'rssi': sum(rssi_values) / len(rssi_values)})
                        current_power += self.power_step_var.get(); time.sleep(0.1)
                    if self.test_running: self.data_queue.put({'type': 'complete'})
                finally:
                    ### ALTERADO: usa self.com_port ###
                    rfid_sdk.UHF_RFID_Close(self.com_port)
                    self.port_manager.release_port("RSSIPower")
        except Exception as e:
            self.data_queue.put({'type': 'error', 'error': f'Erro na varredura: {e}'})
        if not self.test_running: self.data_queue.put({'type': 'stop'})

    def scan_for_tags(self):
        if self.test_running:
            messagebox.showwarning(t('rssi_power.test_running'), t('rssi_power.test_running_scan_msg'))
            return
        
        # NOVO: Validação dos limites da licença antes do scan
        if not self.license_limits.get('is_licensed', False):
            messagebox.showerror(t('rssi_power.invalid_license'), t('rssi_power.invalid_license_msg'), parent=self)
            return
        
        # CORREÇÃO: Validação da frequência antes do scan
        # A frequência mostrada na UI é controlada pelo usuário - não alteramos ela
        user_freq = self.freq_var.get()
        
        # Para o scan de tags (registro), usamos frequência específica baseada na licença
        # mas não alteramos o campo visível - apenas usamos internamente
        try:
            is_licensed = bool(self.license_limits.get('is_licensed', False))
            license_name = str(self.license_limits.get('license_name', '')).upper()
            if is_licensed and 'ANATEL' in license_name:
                scan_freq = 917.0  # Frequência usada apenas para o scan, não altera UI
            elif is_licensed and 'ETSI' in license_name:
                scan_freq = 866.0  # Frequência usada apenas para o scan, não altera UI
            else:
                scan_freq = 915.0  # Frequência usada apenas para o scan, não altera UI
        except Exception:
            scan_freq = user_freq  # Fallback: usa o que o usuário definiu
        
        # Para validação, ainda usa a frequência do usuário (que será testada durante o sweep)
        current_freq = user_freq
        min_freq = self.license_limits.get('min_freq', 800)
        max_freq = self.license_limits.get('max_freq', 1000)
        
        # DEBUG: Mostra os limites atuais da licença
        
        # Validação contra múltiplas faixas
        freq_valid = False
        freq_ranges = self.license_limits.get('freq_ranges', [])
        excluded_ranges = self.license_limits.get('excluded_ranges', [])
        
        if freq_ranges:
            # Valida contra faixas específicas
            for range_min, range_max in freq_ranges:
                if range_min <= current_freq <= range_max:
                    # Verifica se não está em uma faixa excluída
                    in_excluded = False
                    for excl_min, excl_max in excluded_ranges:
                        if excl_min <= current_freq <= excl_max:
                            in_excluded = True
                            break
                    
                    if not in_excluded:
                        freq_valid = True
                        break
                    else:
                        pass
        else:
            # Fallback para validação simples
            freq_valid = (min_freq <= current_freq <= max_freq)
        
        if not freq_valid:
            ranges_str = " ou ".join([f"{r[0]}-{r[1]} MHz" for r in freq_ranges]) if freq_ranges else f"{min_freq}-{max_freq} MHz"
            excluded_str = " e ".join([f"{r[0]}-{r[1]} MHz" for r in excluded_ranges]) if excluded_ranges else "nenhuma"
            messagebox.showerror(t('rssi_power.freq_outside_license'), 
                               t('rssi_power.freq_outside_scan_msg').format(freq=current_freq, allowed=ranges_str, excluded=excluded_str), 
                               parent=self)
            return
        
        with self.com_port_lock:
            ### ALTERADO: usa self.com_port ###
            if rfid_sdk.UHF_RFID_Open(self.com_port, BAUD_RATE) == 0:
                try:
                    # CORREÇÃO: Para registro de tag, usa frequência específica da licença (scan_freq)
                    # Mas o campo na UI permanece com o valor que o usuário escolheu (para o teste depois)
                    self._set_power(15.0); self._set_frequency(scan_freq)
                    self.available_tags = self._scan_for_tags_internal(duration_sec=1.0)
                finally: 
                    ### ALTERADO: usa self.com_port ###
                    rfid_sdk.UHF_RFID_Close(self.com_port)
            else: 
                ### ALTERADO: usa self.com_port ###
                # CORREÇÃO: Não exibe erro de hardware quando sem licença (modo browser)
                is_licensed = self.license_limits.get('is_licensed', False)
                if is_licensed:
                    messagebox.showerror(t('rssi_power.hardware_error'), t('rssi_power.hardware_error_msg').format(port=self.com_port)); return
                else:
                    pass
        if self.available_tags: self.show_tags_in_interface()
        else: messagebox.showinfo(t('rssi_power.tag_search'), t('rssi_power.no_tags_found'))

    # ... (O resto do arquivo não precisa de alterações)
    # O código completo será incluído para que você possa copiar e colar
    def setup_ui(self):
        main_frame = tk.Frame(self)
        main_frame.pack(fill='both', expand=True, padx=10, pady=10)
        main_frame.grid_columnconfigure(1, weight=1)
        main_frame.grid_rowconfigure(0, weight=1)
        main_frame.grid_rowconfigure(1, weight=0)
        
        top_panels_frame = tk.Frame(main_frame)
        top_panels_frame.grid(row=0, column=0, columnspan=2, sticky='nsew')
        top_panels_frame.grid_columnconfigure(1, weight=1)
        top_panels_frame.grid_rowconfigure(0, weight=1)

        left_panel = tk.Frame(top_panels_frame)
        left_panel.grid(row=0, column=0, sticky='ns', padx=(0, 10))
        
        right_panel = tk.Frame(top_panels_frame)
        right_panel.grid(row=0, column=1, sticky='nsew')
        
        self._build_config_panel(left_panel)
        
        # --- Mensagem de Modo Browser ---
        # Posiciona dentro da "Configuração do Teste"
        target_parent = getattr(self, 'config_frame', left_panel)
        self.browser_frame = tk.Frame(target_parent, bg='#f0f8ff')
        
        # Bullet point azul
        bullet_label = tk.Label(self.browser_frame, text="•", fg="blue", font=("Helvetica", 10), bg='#f0f8ff')
        bullet_label.pack(side='left', anchor='w')
        
        # Texto "modo browser"
        self.browser_mode_label = tk.Label(self.browser_frame, text=f" {t('rssi_power.browser_mode')}", fg="blue", font=("Helvetica", 10), bg='#f0f8ff')
        self._widget_refs['browser_mode_label'] = self.browser_mode_label
        self.browser_mode_label.pack(side='left', anchor='w')
        
        # Controle inicial da visibilidade baseado na licença
        if self.license_limits.get('is_licensed', False):
            self.browser_frame.pack_forget()  # Esconde quando há licença
        else:
            self.browser_frame.pack(fill='x', pady=(5, 5), padx=5)  # Mostra quando não há licença
        
        self._build_tags_panel(left_panel)
        self._build_actions_panel(left_panel)
        self._build_graph_panel(right_panel)
        
        if DATABASE_AVAILABLE:
            self._build_history_panel(main_frame)
            self.after(200, self.update_plot_from_history)
        
        self.setup_graph()
        
        # Aplica o estado inicial da UI baseado na licença
        self.update_ui_state()
    
    def refresh_language(self):
        """Atualiza todos os textos da interface quando o idioma é alterado"""
        try:
            # Atualiza títulos de frames
            if 'config_frame' in self._widget_refs:
                self._widget_refs['config_frame'].config(text=t('rssi_power.test_config'))
            if 'tags_frame' in self._widget_refs:
                self._widget_refs['tags_frame'].config(text=t('rssi_power.tag_selection'))
            if 'actions_frame' in self._widget_refs:
                self._widget_refs['actions_frame'].config(text=t('rssi_power.controls'))
            if 'graph_frame' in self._widget_refs:
                self._widget_refs['graph_frame'].config(text=t('rssi_power.graph_title'))
            if 'history_frame' in self._widget_refs:
                self._widget_refs['history_frame'].config(text=t('rssi_power.test_history'))
            
            # Atualiza labels
            if 'name_label' in self._widget_refs:
                self._widget_refs['name_label'].config(text=t('rssi_power.name'))
            if 'freq_label' in self._widget_refs:
                self._widget_refs['freq_label'].config(text=t('rssi_power.frequency_mhz'))
            if 'samples_label' in self._widget_refs:
                self._widget_refs['samples_label'].config(text=t('rssi_power.samples_per_point'))
            if 'power_step_label' in self._widget_refs:
                self._widget_refs['power_step_label'].config(text=t('rssi_power.power_step_dbm'))
            if 'browser_mode_label' in self._widget_refs:
                self._widget_refs['browser_mode_label'].config(text=f" {t('rssi_power.browser_mode')}")
            
            # Atualiza botões
            if 'scan_button' in self._widget_refs:
                self._widget_refs['scan_button'].config(text=t('rssi_power.register_tag'))
            if 'start_test_button' in self._widget_refs:
                self._widget_refs['start_test_button'].config(text=t('rssi_power.test'))
            if 'stop_test_button' in self._widget_refs:
                self._widget_refs['stop_test_button'].config(text=t('rssi_power.stop'))
            if 'clear_plot_button' in self._widget_refs:
                self._widget_refs['clear_plot_button'].config(text=t('rssi_power.clear_plot'))
            if 'save_selected_button' in self._widget_refs:
                self._widget_refs['save_selected_button'].config(text=t('rssi_power.save_selected'))
            if 'import_button' in self._widget_refs:
                self._widget_refs['import_button'].config(text=t('rssi_power.import_tests'))
            if 'generate_report_button' in self._widget_refs:
                self._widget_refs['generate_report_button'].config(text=t('rssi_power.generate_report'))
            if 'select_all_btn' in self._widget_refs:
                self._widget_refs['select_all_btn'].config(text=t('rssi_power.select_all'))
            if 'deselect_all_btn' in self._widget_refs:
                self._widget_refs['deselect_all_btn'].config(text=t('rssi_power.deselect_all'))
            if 'delete_selected_button' in self._widget_refs:
                self._widget_refs['delete_selected_button'].config(text=t('rssi_power.delete_selected'))
            
            # Atualiza labels de status
            if 'selected_tag_label' in self._widget_refs:
                current_text = self._widget_refs['selected_tag_label'].cget('text')
                if "Selecionada:" in current_text or "Selected:" in current_text:
                    # Mantém o texto se houver tag selecionada
                    pass
                else:
                    self._widget_refs['selected_tag_label'].config(text=t('rssi_power.no_tag_selected'))
            if 'stats_label' in self._widget_refs:
                # Atualiza estatísticas ou mantém loading
                current = self._widget_refs['stats_label'].cget('text')
                if 'Carregando' in current or 'Loading' in current:
                    self._widget_refs['stats_label'].config(text=t('rssi_power.stats_loading'))
                else:
                    # Atualiza as estatísticas com a tradução correta
                    self.update_history_stats()
            if 'zoom_label' in self._widget_refs:
                # Zoom label é atualizado dinamicamente, pega o valor atual
                current = self._widget_refs['zoom_label'].cget('text')
                # Extrai o valor numérico se existir
                import re
                match = re.search(r'(\d+\.?\d*)x', current)
                if match:
                    zoom_val = match.group(1)
                    self._widget_refs['zoom_label'].config(text=f"{t('rssi_power.zoom')} {zoom_val}x")
                else:
                    self._widget_refs['zoom_label'].config(text=f"{t('rssi_power.zoom')} 1.0x")
            
            # Atualiza cabeçalhos da tabela
            if hasattr(self, 'history_tree'):
                self.history_tree.heading("Plot", text=f"{t('rssi_power.plot')} ↕")
                self.history_tree.heading("Nome", text=f"{t('rssi_power.name_col')} ↕")
                self.history_tree.heading("EPC da Tag", text=f"{t('rssi_power.tag_epc')} ↕")
                self.history_tree.heading("Frequência", text=f"{t('rssi_power.frequency_col')} ↕")
                self.history_tree.heading("Range Potência", text=f"{t('rssi_power.power_range')} ↕")
                self.history_tree.heading("Range RSSI", text=f"{t('rssi_power.rssi_range')} ↕")
                self.history_tree.heading("Slope", text=f"{t('rssi_power.slope')} ↕")
                self.history_tree.heading("Data/Hora", text=f"{t('rssi_power.date_time')} ↕")
        except Exception as e:
            print(f"⚠️ Erro ao atualizar idioma no RSSI x Power: {e}")

    def _build_config_panel(self, parent):
        config_frame = tk.LabelFrame(parent, text=t('rssi_power.test_config'), padx=10, pady=5)
        config_frame.pack(fill='x', pady=(0, 10))
        # Torna acessível para outros métodos (mensagem de modo browser)
        self.config_frame = config_frame
        self._widget_refs['config_frame'] = config_frame
        
        name_label = tk.Label(config_frame, text=t('rssi_power.name'))
        name_label.pack(anchor='w')
        self._widget_refs['name_label'] = name_label
        
        self.test_name_entry = ttk.Entry(config_frame, textvariable=self.test_name_var)
        self.test_name_entry.pack(fill='x', pady=(0, 5))
        
        freq_label = tk.Label(config_frame, text=t('rssi_power.frequency_mhz'))
        freq_label.pack(anchor='w')
        self._widget_refs['freq_label'] = freq_label
        self.freq_spinbox = ttk.Spinbox(config_frame, from_=self.license_limits['min_freq'], to=self.license_limits['max_freq'], increment=1.0, textvariable=self.freq_var, wrap=True)
        self.freq_spinbox.pack(fill='x', pady=(0, 5))
        # Salva automaticamente quando o usuário muda a frequência
        try:
            self.freq_var.trace_add('write', lambda *args: self._save_persistent_settings())
        except Exception:
            pass
        
        samples_label = tk.Label(config_frame, text=t('rssi_power.samples_per_point'))
        samples_label.pack(anchor='w')
        self._widget_refs['samples_label'] = samples_label
        
        self.samples_spinbox = ttk.Spinbox(config_frame, from_=1, to=20, textvariable=self.samples_var, wrap=True)
        self.samples_spinbox.pack(fill='x', pady=(0, 5))
        try:
            self.samples_var.trace_add('write', lambda *args: self._save_persistent_settings())
        except Exception:
            pass
        
        power_step_label = tk.Label(config_frame, text=t('rssi_power.power_step_dbm'))
        power_step_label.pack(anchor='w')
        self._widget_refs['power_step_label'] = power_step_label
        self.power_step_spinbox = ttk.Spinbox(config_frame, from_=0.5, to=2.0, increment=0.5, textvariable=self.power_step_var, wrap=True)
        self.power_step_spinbox.pack(fill='x')
        try:
            self.power_step_var.trace_add('write', lambda *args: self._save_persistent_settings())
        except Exception:
            pass

    def _load_persistent_settings(self):
        """Carrega frequência/samples/passo salvos em disco e aplica na UI"""
        if os.path.exists(self.settings_file):
            with open(self.settings_file, 'r', encoding='utf-8') as f:
                data = json.load(f)
            freq = data.get('freq_mhz')
            samples = data.get('samples')
            step = data.get('power_step')
            if isinstance(freq, (int, float)):
                try: self.freq_var.set(float(freq))
                except Exception: pass
            if isinstance(samples, int) and 1 <= samples <= 20:
                try: self.samples_var.set(samples)
                except Exception: pass
            if isinstance(step, (int, float)):
                try: self.power_step_var.set(float(step))
                except Exception: pass
            print(f"✅ RSSI x Power: Configurações carregadas: freq={self.freq_var.get()}, samples={self.samples_var.get()}, step={self.power_step_var.get()}")

    def _save_persistent_settings(self):
        """Salva frequência/samples/passo atuais em disco"""
        try:
            data = {
                'freq_mhz': float(self.freq_var.get()),
                'samples': int(self.samples_var.get()),
                'power_step': float(self.power_step_var.get()),
                'saved_at': datetime.now().isoformat()
            }
            with open(self.settings_file, 'w', encoding='utf-8') as f:
                json.dump(data, f, indent=2, ensure_ascii=False)
        except Exception:
            pass

    def _build_tags_panel(self, parent):
        tags_frame = tk.LabelFrame(parent, text=t('rssi_power.tag_selection'), padx=10, pady=5)
        tags_frame.pack(fill='x', pady=(0, 10))
        self._widget_refs['tags_frame'] = tags_frame
        
        self.scan_button = ttk.Button(tags_frame, text=t('rssi_power.register_tag'), command=self.scan_for_tags)
        self.scan_button.pack(fill='x', pady=(0, 5))
        self._widget_refs['scan_button'] = self.scan_button
        
        self.selected_tag_label = tk.Label(tags_frame, text=t('rssi_power.no_tag_selected'), fg="gray")
        self.selected_tag_label.pack(fill='x')
        self._widget_refs['selected_tag_label'] = self.selected_tag_label
        
        self.tags_checkboxes_frame = tk.Frame(tags_frame)
        self.tags_checkboxes_frame.pack(fill='x', pady=(5,0))

    def _build_actions_panel(self, parent):
        actions_frame = tk.LabelFrame(parent, text=t('rssi_power.controls'), padx=10, pady=5)
        actions_frame.pack(fill='x')
        self._widget_refs['actions_frame'] = actions_frame

        self.start_test_button = ttk.Button(actions_frame, text=t('rssi_power.test'), command=self.start_test, state="disabled")
        self.start_test_button.pack(fill='x', pady=2)
        self._widget_refs['start_test_button'] = self.start_test_button
        
        self.stop_test_button = ttk.Button(actions_frame, text=t('rssi_power.stop'), command=self.stop_test, state="disabled")
        self.stop_test_button.pack(fill='x', pady=2)
        self._widget_refs['stop_test_button'] = self.stop_test_button
        
        self.clear_plot_button = ttk.Button(actions_frame, text=t('rssi_power.clear_plot'), command=self.clear_plot_and_selection)
        self.clear_plot_button.pack(fill='x', pady=2)
        self._widget_refs['clear_plot_button'] = self.clear_plot_button
        
        self.save_selected_button = ttk.Button(actions_frame, text=t('rssi_power.save_selected'), command=self.save_selected_tests)
        self.save_selected_button.pack(fill='x', pady=2)
        self._widget_refs['save_selected_button'] = self.save_selected_button
        
        self.import_button = ttk.Button(actions_frame, text=t('rssi_power.import_tests'), command=self.import_tests)
        self.import_button.pack(fill='x', pady=2)
        self._widget_refs['import_button'] = self.import_button
        
        self.generate_report_button = ttk.Button(actions_frame, text=t('rssi_power.generate_report'), command=self.generate_pdf_report, state="disabled")
        self.generate_report_button.pack(fill='x', pady=2)
        self._widget_refs['generate_report_button'] = self.generate_report_button
        
    def _build_graph_panel(self, parent):
        self.graph_frame = tk.LabelFrame(parent, text=t('rssi_power.graph_title'), padx=10, pady=5)
        self._widget_refs['graph_frame'] = self.graph_frame
        self.graph_frame.pack(fill='both', expand=True)
        # Controles de zoom (igual padrão Noise/Antenna)
        self._setup_zoom_controls(self.graph_frame)

    def _build_history_panel(self, parent):
        history_frame = ttk.LabelFrame(parent, text=t('rssi_power.test_history'))
        history_frame.grid(row=1, column=0, columnspan=2, sticky='ew', padx=0, pady=(10, 0))
        self._widget_refs['history_frame'] = history_frame
        
        stats_frame = ttk.Frame(history_frame)
        stats_frame.pack(fill="x", pady=2, padx=5)
        self.stats_label = ttk.Label(stats_frame, text=t('rssi_power.stats_loading'))
        self.stats_label.pack(side="left")
        self._widget_refs['stats_label'] = self.stats_label
        
        tree_frame = ttk.Frame(history_frame)
        tree_frame.pack(fill='x', expand=True, padx=5)
        
        columns = ("Plot", "ID", "Nome", "EPC da Tag", "Frequência", "Range Potência", "Range RSSI", "Slope", "Data/Hora")
        display_columns = ("Plot", "Nome", "EPC da Tag", "Frequência", "Range Potência", "Range RSSI", "Slope", "Data/Hora")

        self.history_tree = ttk.Treeview(tree_frame, columns=columns, displaycolumns=display_columns, show="headings", height=5)
        
        self.history_tree.heading("Plot", text=f"{t('rssi_power.plot')} ↕", command=lambda: self.sort_treeview("Plot")); self.history_tree.column("Plot", width=40, anchor='center')
        self.history_tree.heading("Nome", text=f"{t('rssi_power.name_col')} ↕", command=lambda: self.sort_treeview("Nome")); self.history_tree.column("Nome", width=150)
        self.history_tree.heading("EPC da Tag", text=f"{t('rssi_power.tag_epc')} ↕", command=lambda: self.sort_treeview("EPC da Tag")); self.history_tree.column("EPC da Tag", width=200)  # NOVO: Largura aumentada para EPC completo
        self.history_tree.heading("Frequência", text=f"{t('rssi_power.frequency_col')} ↕", command=lambda: self.sort_treeview("Frequência")); self.history_tree.column("Frequência", width=80, anchor='center')
        self.history_tree.heading("Range Potência", text=f"{t('rssi_power.power_range')} ↕", command=lambda: self.sort_treeview("Range Potência")); self.history_tree.column("Range Potência", width=120, anchor='center')
        self.history_tree.heading("Range RSSI", text=f"{t('rssi_power.rssi_range')} ↕", command=lambda: self.sort_treeview("Range RSSI")); self.history_tree.column("Range RSSI", width=120, anchor='center')
        self.history_tree.heading("Slope", text=f"{t('rssi_power.slope')} ↕", command=lambda: self.sort_treeview("Slope")); self.history_tree.column("Slope", width=100, anchor='center')
        self.history_tree.heading("Data/Hora", text=f"{t('rssi_power.date_time')} ↕", command=lambda: self.sort_treeview("Data/Hora")); self.history_tree.column("Data/Hora", width=150, anchor='center')

        scrollbar = ttk.Scrollbar(tree_frame, orient="vertical", command=self.history_tree.yview)
        self.history_tree.configure(yscrollcommand=scrollbar.set)
        
        self.history_tree.pack(side='left', fill='x', expand=True)
        scrollbar.pack(side='right', fill='y')

        actions_frame = ttk.Frame(history_frame)
        actions_frame.pack(fill='x', padx=5, pady=5)
        select_all_btn = ttk.Button(actions_frame, text=t('rssi_power.select_all'), command=self.select_all_tests)
        select_all_btn.pack(side='left', padx=(0,5))
        self._widget_refs['select_all_btn'] = select_all_btn
        
        deselect_all_btn = ttk.Button(actions_frame, text=t('rssi_power.deselect_all'), command=self.deselect_all_tests)
        deselect_all_btn.pack(side='left', padx=(0,5))
        self._widget_refs['deselect_all_btn'] = deselect_all_btn
        
        self.delete_selected_button = ttk.Button(actions_frame, text=t('rssi_power.delete_selected'), command=self.delete_selected_tests)
        self._widget_refs['delete_selected_button'] = self.delete_selected_button
        self.delete_selected_button.pack(side='left', padx=(0,5))

        self.history_tree.bind('<Button-1>', self.on_history_tree_click)
        # NOVO: Adiciona evento de duplo-clique para edição in-line
        self.history_tree.bind('<Double-1>', self.on_history_tree_double_click)
        self.load_history_to_tree()

    def setup_graph(self):
        # Mantém os controles de zoom no topo; remove apenas o canvas anterior
        for widget in list(self.graph_frame.winfo_children()):
            try:
                if getattr(widget, '_is_zoom_controls', False):
                    continue
            except Exception:
                pass
            widget.destroy()
        self.fig = Figure(figsize=(10, 8), dpi=100)
        self.ax = self.fig.add_subplot(111)
        self.canvas = FigureCanvasTkAgg(self.fig, self.graph_frame)
        self.canvas.draw()
        self.canvas.get_tk_widget().pack(fill='both', expand=True)
        # Estado de zoom
        self.zoom_factor = 1.0
        self._current_xlim = None
        self._current_ylim = None
        
        # NOVO: Cria tooltip annotation para o gráfico
        self.annot = self.ax.annotate("", xy=(0, 0), xytext=(20, 20), textcoords="offset points",
                                       bbox=dict(boxstyle="round,pad=0.5", 
                                                facecolor="lightblue", 
                                                alpha=0.9,
                                                edgecolor="navy",
                                                linewidth=1),
                                       arrowprops=dict(arrowstyle="->", 
                                                      connectionstyle="arc3,rad=0",
                                                      color="navy",
                                                      lw=1),
                                       fontsize=9, 
                                       ha='left',
                                       va='bottom',
                                       wrap=True)
        self.annot.set_visible(False)
        
        # NOVO: Configura interatividade do tooltip
        self.setup_interactivity()
        
        self.update_plot()
        # Garante que os botões fiquem visíveis sobre o canvas
        try:
            for widget in self.graph_frame.winfo_children():
                if getattr(widget, '_is_zoom_controls', False):
                    widget.lift()
        except Exception:
            pass
    
    def update_plot(self):
        # Preserva limites atuais se já houver zoom aplicado
        preserve_xlim = getattr(self, '_current_xlim', None)
        preserve_ylim = getattr(self, '_current_ylim', None)
        self.ax.clear()
        self.ax.set_xlabel(t('rssi_power.graph_power_label')); self.ax.set_ylabel(t('rssi_power.graph_rssi_label'))
        self.ax.grid(True, alpha=0.3)
        min_power, max_power = self.license_limits['min_power'], self.license_limits['max_power']
        # Defaults
        self.ax.set_xlim(min_power - 1, max_power + 1); self.ax.set_ylim(-80, -20)
        # Reaplica limites preservados (zoom)
        if preserve_xlim is not None and preserve_ylim is not None:
            self.ax.set_xlim(preserve_xlim); self.ax.set_ylim(preserve_ylim)
        

        # CORREÇÃO: Cores com alto contraste - sem cores claras
        colors = ['#1f77b4', '#d62728', '#2ca02c', '#ff7f0e', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf']
        
        # NOVO: Plota testes históricos com cores persistentes
        for test_name, test_info in self.multiple_tests.items():
            test_data = test_info['data']
            test_id = test_info['id']
            
            # Usa cor persistente se disponível, senão atribui uma nova
            if test_id not in self.test_colors:
                self.test_colors[test_id] = colors[len(self.test_colors) % len(colors)]
            color = self.test_colors[test_id]
            
            self.ax.plot(test_data['power'], test_data['rssi'], 'o-', linewidth=0.8, markersize=2, alpha=0.7, label=test_name, color=color)

        if self.test_running and self.live_power_data and self.live_rssi_data:
            # NOVO: Usa cor consistente para teste ao vivo
            current_test_name = self.test_name_var.get()
            temp_id = abs(hash(current_test_name)) % 1000
            
            # Atribui cor se não existir
            if temp_id not in self.test_colors:
                self.test_colors[temp_id] = '#d62728'  # Vermelho para teste ao vivo
            
            color = self.test_colors[temp_id]
            self.ax.plot(self.live_power_data, self.live_rssi_data, 'o-', linewidth=0.8, markersize=2, label=current_test_name + t('rssi_power.live'), color=color)
        
        # Linha vermelha horizontal progressiva em Y=-80dBm (só aparece durante o teste)
        min_power, max_power = self.license_limits['min_power'], self.license_limits['max_power']
        # CORREÇÃO: Se houver pontos plotados, usa o último ponto (sincronizado). Caso contrário, usa current_power para mostrar progresso inicial
        if self.test_running:
            if self.live_power_data:
                # Se há pontos plotados, sincroniza com o último ponto (não adianta)
                ref_power = self.live_power_data[-1]
            elif self.current_power is not None:
                # Se não há pontos ainda, mostra onde está sendo medida (progresso inicial)
                ref_power = self.current_power
            else:
                ref_power = min_power
            # CORREÇÃO: Usa coordenadas absolutas do eixo X, não normalizadas
            # Desenha a linha de min_power até ref_power em Y=-80
            if ref_power >= min_power:
                self.ax.hlines(y=-80, xmin=min_power, xmax=ref_power, color='red', linewidth=4, alpha=0.9, linestyle='-', label='Progresso -80dBm')
        
        # NOVO: Mostra legenda se houver testes em multiple_tests ou teste em andamento
        if self.multiple_tests or (self.test_running and (self.live_power_data or self.current_power is not None)):
            self.ax.legend(framealpha=0.9, fontsize='small')

        # NOVO: Recria a anotação do tooltip após cada atualização do gráfico
        self.annot = self.ax.annotate("", xy=(0, 0), xytext=(20, 20), textcoords="offset points",
                                       bbox=dict(boxstyle="round,pad=0.5", 
                                                facecolor="lightblue", 
                                                alpha=0.9,
                                                edgecolor="navy",
                                                linewidth=1),
                                       arrowprops=dict(arrowstyle="->", 
                                                      connectionstyle="arc3,rad=0",
                                                      color="navy",
                                                      lw=1),
                                       fontsize=9, 
                                       ha='left',
                                       va='bottom',
                                       wrap=True)
        self.annot.set_visible(False)

        self.canvas.draw()

    def _setup_zoom_controls(self, parent):
        """Configura os controles de zoom na parte superior esquerda do gráfico"""
        zoom_frame = ttk.Frame(parent)
        # Usa 'place' para não misturar com pack no mesmo container
        zoom_frame.place(x=5, y=5, anchor='nw')
        # marca para não ser removido em setup_graph
        zoom_frame._is_zoom_controls = True
        tk.Button(zoom_frame, text="🔍+", width=3, height=1, command=self._zoom_in, font=("Arial", 10)).grid(row=0, column=0, padx=2)
        tk.Button(zoom_frame, text="🔍-", width=3, height=1, command=self._zoom_out, font=("Arial", 10)).grid(row=0, column=1, padx=2)
        tk.Button(zoom_frame, text="↻", width=3, height=1, command=self._reset_zoom, font=("Arial", 10)).grid(row=0, column=2, padx=2)
        tk.Button(zoom_frame, text="📐", width=3, height=1, command=self._fit_to_data, font=("Arial", 10)).grid(row=0, column=3, padx=2)
        # Botão Pan (arrastar)
        self.pan_active = False
        self.pan_btn = tk.Button(zoom_frame, text="✋", width=3, height=1,
                                 command=self._toggle_pan, font=("Arial", 10),
                                 relief=tk.RAISED)
        self.pan_btn.grid(row=0, column=4, padx=2)
        self.zoom_label = tk.Label(zoom_frame, text=f"{t('rssi_power.zoom')} 1.0x", font=("Arial", 8), fg="gray")
        self.zoom_label.grid(row=0, column=5, padx=5)
        self._widget_refs['zoom_label'] = self.zoom_label

    def _zoom_in(self):
        try:
            xlim = self.ax.get_xlim(); ylim = self.ax.get_ylim()
            x_center = (xlim[0] + xlim[1]) / 2; y_center = (ylim[0] + ylim[1]) / 2
            x_range = (xlim[1] - xlim[0]) * 0.8; y_range = (ylim[1] - ylim[0]) * 0.8
            self._current_xlim = (x_center - x_range/2, x_center + x_range/2)
            self._current_ylim = (y_center - y_range/2, y_center + y_range/2)
            self.ax.set_xlim(self._current_xlim); self.ax.set_ylim(self._current_ylim)
            self.zoom_factor = (self.zoom_factor or 1.0) * 1.25
            if hasattr(self, 'zoom_label'): self.zoom_label.config(text=f"{t('rssi_power.zoom')} {self.zoom_factor:.1f}x")
            self.canvas.draw()
        except Exception as e:
            pass

    def _zoom_out(self):
        try:
            xlim = self.ax.get_xlim(); ylim = self.ax.get_ylim()
            x_center = (xlim[0] + xlim[1]) / 2; y_center = (ylim[0] + ylim[1]) / 2
            x_range = (xlim[1] - xlim[0]) / 0.8; y_range = (ylim[1] - ylim[0]) / 0.8
            self._current_xlim = (x_center - x_range/2, x_center + x_range/2)
            self._current_ylim = (y_center - y_range/2, y_center + y_range/2)
            self.ax.set_xlim(self._current_xlim); self.ax.set_ylim(self._current_ylim)
            self.zoom_factor = max(1.0, (self.zoom_factor or 1.0) * 0.8)
            if hasattr(self, 'zoom_label'): self.zoom_label.config(text=f"{t('rssi_power.zoom')} {self.zoom_factor:.1f}x")
            self.canvas.draw()
        except Exception as e:
            pass

    def _reset_zoom(self):
        try:
            min_power, max_power = self.license_limits.get('min_power', 5), self.license_limits.get('max_power', 25)
            self._current_xlim = (min_power - 1, max_power + 1)
            self._current_ylim = (-80, -20)
            self.ax.set_xlim(self._current_xlim); self.ax.set_ylim(self._current_ylim)
            self.zoom_factor = 1.0
            if hasattr(self, 'zoom_label'): self.zoom_label.config(text=f"{t('rssi_power.zoom')} 1.0x")
            self.canvas.draw()
        except Exception as e:
            pass

    def _fit_to_data(self):
        try:
            lines = self.ax.get_lines()
            if not lines:
                return
            x_data = []; y_data = []
            for line in lines:
                x_line = line.get_xdata(); y_line = line.get_ydata()
                if len(x_line) >= 2:
                    x_data.extend(x_line); y_data.extend(y_line)
            if not x_data:
                return
            x_min, x_max = min(x_data), max(x_data); y_min, y_max = min(y_data), max(y_data)
            x_range = x_max - x_min; y_range = y_max - y_min
            x_margin = 0.5 if x_range < 10 else x_range * 0.05
            y_margin = 1.0 if y_range < 10 else y_range * 0.1
            self._current_xlim = (x_min - x_margin, x_max + x_margin)
            self._current_ylim = (y_min - y_margin, y_max + y_margin)
            self.ax.set_xlim(self._current_xlim); self.ax.set_ylim(self._current_ylim)
            self.zoom_factor = 1.0
            if hasattr(self, 'zoom_label'): self.zoom_label.config(text=f"{t('rssi_power.zoom')} 1.0x")
            self.canvas.draw()
        except Exception as e:
            pass
    
    def _toggle_pan(self):
        """Ativa/desativa o modo pan (arrastar gráfico)"""
        self.pan_active = not self.pan_active
        
        if self.pan_active:
            # Ativa modo pan
            self.pan_btn.config(relief=tk.SUNKEN, bg='#d0d0d0')
            self.canvas.mpl_connect('button_press_event', self._on_pan_press)
            self.canvas.mpl_connect('button_release_event', self._on_pan_release)
            self.canvas.mpl_connect('motion_notify_event', self._on_pan_motion)
            self.pan_start = None
            pass
        else:
            # Desativa modo pan
            self.pan_btn.config(relief=tk.RAISED, bg='SystemButtonFace')

    def _on_pan_press(self, event):
        """Callback quando pressiona o mouse no modo pan"""
        if self.pan_active and event.inaxes == self.ax:
            self.pan_start = (event.xdata, event.ydata)
            self.pan_xlim = self.ax.get_xlim()
            self.pan_ylim = self.ax.get_ylim()

    def _on_pan_release(self, event):
        """Callback quando solta o mouse no modo pan"""
        if self.pan_active:
            # OTIMIZAÇÃO HÍBRIDA: Aplica posição final exata
            if hasattr(self, '_pan_smooth_buffer'):
                # Aplica a posição final exata (sem suavização)
                self.ax.set_xlim(self._pan_smooth_buffer['xlim'])
                self.ax.set_ylim(self._pan_smooth_buffer['ylim'])
                self.canvas.draw_idle()
                # Limpa o buffer
                delattr(self, '_pan_smooth_buffer')
            self.pan_start = None

    def _on_pan_motion(self, event):
        """Callback quando move o mouse no modo pan"""
        if self.pan_active and self.pan_start and event.inaxes == self.ax:
            # Calcula deslocamento
            dx = self.pan_start[0] - event.xdata
            dy = self.pan_start[1] - event.ydata
            
            # Aplica deslocamento aos limites
            new_xlim = (self.pan_xlim[0] + dx, self.pan_xlim[1] + dx)
            new_ylim = (self.pan_ylim[0] + dy, self.pan_ylim[1] + dy)
            
            # OTIMIZAÇÃO HÍBRIDA: Throttling agressivo + suavização para eliminar tremulação
            import time
            current_time = time.time()
            if not hasattr(self, '_last_pan_update'):
                self._last_pan_update = 0
                self._pan_smooth_buffer = {'xlim': new_xlim, 'ylim': new_ylim}
            
            # Atualiza apenas a cada 50ms (20 FPS) para reduzir tremulação drasticamente
            if current_time - self._last_pan_update >= 0.05:
                # Suavização: interpola entre posição atual e nova posição
                if hasattr(self, '_pan_smooth_buffer'):
                    # Interpolação suave (70% da nova posição + 30% da anterior)
                    smooth_xlim = (
                        self._pan_smooth_buffer['xlim'][0] * 0.3 + new_xlim[0] * 0.7,
                        self._pan_smooth_buffer['xlim'][1] * 0.3 + new_xlim[1] * 0.7
                    )
                    smooth_ylim = (
                        self._pan_smooth_buffer['ylim'][0] * 0.3 + new_ylim[0] * 0.7,
                        self._pan_smooth_buffer['ylim'][1] * 0.3 + new_ylim[1] * 0.7
                    )
                else:
                    smooth_xlim = new_xlim
                    smooth_ylim = new_ylim
                
                self.ax.set_xlim(smooth_xlim)
                self.ax.set_ylim(smooth_ylim)
                self.canvas.draw_idle()
                self._last_pan_update = current_time
                self._pan_smooth_buffer = {'xlim': smooth_xlim, 'ylim': smooth_ylim}
    
    def setup_interactivity(self):
        """Configura a interatividade do tooltip no gráfico"""
        if hasattr(self, 'canvas'):
            self.canvas.mpl_connect('motion_notify_event', self._on_hover)
    
    def _on_hover(self, event):
        """Manipula o evento de hover do mouse sobre o gráfico"""
        if event.inaxes != self.ax:
            if self.annot and self.annot.get_visible():
                self.annot.set_visible(False)
                self.canvas.draw_idle()
            return
        
        for line in self.ax.lines:
            cont, ind = line.contains(event)
            if cont:
                pos = line.get_xydata()[ind["ind"][0]]
                x_coord, y_coord = pos[0], pos[1]
                tooltip_text = f"{x_coord:.1f} dBm → {y_coord:.1f} dBm"
                self._position_tooltip_smartly(event, x_coord, y_coord, tooltip_text)
                return
        
        if self.annot and self.annot.get_visible():
            self.annot.set_visible(False)
            self.canvas.draw_idle()

    def _position_tooltip_smartly(self, event, x_pos, y_pos, tooltip_text):
        """Posiciona o tooltip de forma inteligente baseado na posição do mouse"""
        try:
            # Obtém dimensões do canvas em pixels
            canvas_width = self.canvas.get_width_height()[0]
            canvas_height = self.canvas.get_width_height()[1]
            
            # Converte coordenadas do evento para pixels do canvas
            x_pixel = event.x
            y_pixel = event.y
            
            # Margens mais conservadoras para evitar cortes
            margin_right = 300   # Margem para borda direita
            margin_top = 150     # Margem para borda superior
            margin_bottom = 150  # Margem para borda inferior
            margin_left = 250    # Margem para borda esquerda
            
            # Calcula posição do tooltip baseada na posição do mouse
            if x_pixel > canvas_width - margin_right:  # Próximo à borda direita
                # Posiciona à esquerda do mouse com margem maior
                xytext = (-150, 20)
                ha = 'right'
            elif x_pixel < margin_left:  # Próximo à borda esquerda
                # Posiciona à direita do mouse com margem maior
                xytext = (150, 20)
                ha = 'left'
            else:  # Posição normal (centro)
                # Posiciona à direita do mouse
                xytext = (20, 20)
                ha = 'left'
            
            # Ajusta posição vertical se necessário
            if y_pixel < margin_top:  # Próximo ao topo
                xytext = (xytext[0], 150)  # Margem maior para baixo
            elif y_pixel > canvas_height - margin_bottom:  # Próximo à parte inferior
                xytext = (xytext[0], -150)  # Margem maior para cima
            
            # Atualiza o tooltip com posicionamento inteligente
            self.annot.xy = (x_pos, y_pos)
            self.annot.xytext = xytext
            self.annot.set_text(tooltip_text)
            self.annot.set_ha(ha)
            self.annot.set_visible(True)
            
            # Força redesenho para garantir que o tooltip apareça
            self.canvas.draw_idle()
            
        except Exception as e:
            # Fallback para posicionamento padrão
            try:
                self.annot.xy = (x_pos, y_pos)
                self.annot.set_text(tooltip_text)
                self.annot.set_visible(True)
                self.canvas.draw_idle()
            except Exception as fallback_error:
                pass
        
    def check_duplicate_test_name(self, test_name):
        """
        Verifica se o nome do teste já existe no histórico
        
        Args:
            test_name: Nome do teste a verificar
            
        Returns:
            bool: True se o nome já existe, False caso contrário
        """
        if not self.database:
            return False
        
        try:
            history = self.database.get_test_history()
            # NOVO: Compara nomes normalizados (sem espaços extras, case-insensitive)
            test_name_normalized = test_name.strip().lower()
            existing_names = [test.get('test_name', '').strip().lower() for test in history]
            return test_name_normalized in existing_names
        except Exception as e:
            return False

    def check_duplicate_test_name_excluding_current(self, test_name, current_test_id):
        """
        Verifica se o nome do teste já existe no histórico, excluindo o teste atual
        
        Args:
            test_name: Nome do teste a verificar
            current_test_id: ID do teste atual que está sendo editado
            
        Returns:
            bool: True se o nome já existe em outro teste, False caso contrário
        """
        if not self.database:
            return False
        
        try:
            history = self.database.get_test_history()
            # NOVO: Compara nomes normalizados (sem espaços extras, case-insensitive)
            test_name_normalized = test_name.strip().lower()
            
            for test in history:
                if test.get('id') != current_test_id:  # Exclui o teste atual
                    existing_name = test.get('test_name', '').strip().lower()
                    if existing_name == test_name_normalized:
                        return True
            return False
        except Exception as e:
            return False

    def start_test(self):
        if self.demo_mode:
            messagebox.showinfo(t('rssi_power.demo_mode'), t('rssi_power.demo_mode_msg'))
            return
        
        # PROTEÇÃO DE TEMPERATURA: Verifica antes de iniciar
        temp = self.get_temperature()
        if temp is not None and temp >= TEMPERATURE_LIMIT:
            messagebox.showerror(t('rssi_power.safety_stop'), 
                               t('rssi_power.safety_stop_msg').format(temp=f"{temp:.1f}", limit=TEMPERATURE_LIMIT), 
                               parent=self)
            return
        
        # Bloqueio em modo browser: não iniciar teste sem licença
        if not self.license_limits.get('is_licensed', False):
            messagebox.showwarning(t('rssi_power.browser_warning'), t('rssi_power.browser_warning_msg'))
            return
        
        # CORREÇÃO: Validação da frequência contra múltiplas faixas da licença
        current_freq = self.freq_var.get()
        min_freq = self.license_limits.get('min_freq', 800)
        max_freq = self.license_limits.get('max_freq', 1000)
        
        # DEBUG: Mostra os limites atuais da licença
        
        # Validação contra múltiplas faixas
        freq_valid = False
        freq_ranges = self.license_limits.get('freq_ranges', [])
        excluded_ranges = self.license_limits.get('excluded_ranges', [])
        
        if freq_ranges:
            # Valida contra faixas específicas
            for range_min, range_max in freq_ranges:
                if range_min <= current_freq <= range_max:
                    # Verifica se não está em uma faixa excluída
                    in_excluded = False
                    for excl_min, excl_max in excluded_ranges:
                        if excl_min <= current_freq <= excl_max:
                            in_excluded = True
                            break
                    
                    if not in_excluded:
                        freq_valid = True
                        break
                    else:
                        pass
        else:
            # Fallback para validação simples
            freq_valid = (min_freq <= current_freq <= max_freq)
        
        if not freq_valid:
            ranges_str = " ou ".join([f"{r[0]}-{r[1]} MHz" for r in freq_ranges]) if freq_ranges else f"{min_freq}-{max_freq} MHz"
            excluded_str = " e ".join([f"{r[0]}-{r[1]} MHz" for r in excluded_ranges]) if excluded_ranges else "nenhuma"
            messagebox.showerror(t('rssi_power.freq_outside_license'), 
                               t('rssi_power.freq_outside_msg').format(freq=current_freq, allowed=ranges_str, excluded=excluded_str), 
                               parent=self)
            return
        
        # CORREÇÃO CRÍTICA: Validação da potência contra limites da licença
        license_min_power = self.license_limits.get('min_power', 5)
        license_max_power = self.license_limits.get('max_power', 25)
        
        # CORREÇÃO: O sweep sempre vai de min_power até max_power da licença, então não precisa validar o range
        # A validação real é feita durante o sweep, onde cada potência individual é verificada
        
        if not self.selected_tag_info:
            messagebox.showwarning(t('rssi_power.no_tag'), t('rssi_power.no_tag_msg'), parent=self); return
        
        # NOVO: Validação de nome do teste (obrigatório)
        current_test_name = self.test_name_var.get().strip()
        if not current_test_name:
            messagebox.showerror(t('rssi_power.test_name_required'), 
                               t('rssi_power.test_name_required_msg'), 
                               parent=self)
            return
        
        # NOVO: Verifica se o nome do teste já existe
        if self.check_duplicate_test_name(current_test_name):
            messagebox.showerror(t('rssi_power.name_duplicate'), 
                               t('rssi_power.name_duplicate_msg').format(name=current_test_name), 
                               parent=self)
            return
        
        if self.test_running:
            messagebox.showwarning(t('rssi_power.test_running'), t('rssi_power.test_running_msg'), parent=self); return

        # NOVO: Define o nome do teste atual para persistência
        self.current_test_name = current_test_name
        
        self.live_power_data, self.live_rssi_data = [], []
        self.test_running = True
        self.update_ui_state()
        
        self.sweep_thread = threading.Thread(target=self.power_sweep_worker, daemon=True)
        self.sweep_thread.start()
            
    def stop_test(self):
        if not self.test_running: return
        self.test_running = False

    def update_ui_state(self):
        is_running = self.test_running
        is_licensed = self.license_limits.get('is_licensed', False)
        
        # CORREÇÃO: Controle baseado na licença E no estado do teste
        if is_licensed:
            # Com licença, botões funcionam normalmente baseado no estado do teste
            self.start_test_button.config(state="disabled" if is_running else "normal" if self.selected_tag_info else "disabled")
            self.stop_test_button.config(state="normal" if is_running else "disabled")
            self.scan_button.config(state="disabled" if is_running else "normal")
            self.save_selected_button.config(state="normal")
            self.generate_report_button.config(state="normal")
            self.delete_selected_button.config(state="normal")
            
            # Campos de entrada habilitados quando não há teste em execução
            for child in self.winfo_children():
                if isinstance(child, (ttk.Entry, ttk.Spinbox)):
                    child.config(state="disabled" if is_running else "normal")
        else:
            # Sem licença (modo browser): bloquear apenas ações que acionam o reader
            self.start_test_button.config(state="disabled")
            self.stop_test_button.config(state="disabled")
            self.scan_button.config(state="disabled")
            # Ações de browser: manter habilitadas
            self.save_selected_button.config(state="normal")
            self.generate_report_button.config(state="normal")
            self.delete_selected_button.config(state="normal")
            
            # Permitir edição de campos em modo browser
            if hasattr(self, 'test_name_entry'):
                self.test_name_entry.config(state="normal")
            if hasattr(self, 'freq_spinbox'):
                self.freq_spinbox.config(state="normal")
            if hasattr(self, 'samples_spinbox'):
                self.samples_spinbox.config(state="normal")
            if hasattr(self, 'power_step_spinbox'):
                self.power_step_spinbox.config(state="normal")
            for child in self.winfo_children():
                if isinstance(child, (ttk.Entry, ttk.Spinbox)):
                    child.config(state="normal")
        
        # Controle da mensagem "• modo browser"
        if hasattr(self, 'browser_frame'):
            if is_licensed:
                self.browser_frame.pack_forget()  # Esconde quando há licença
            else:
                self.browser_frame.pack(fill='x', pady=(5, 5), padx=5)  # Mostra quando não há licença
        


    def process_data_queue(self):
        try:
            # Drena a fila completa a cada tick para manter ordem e evitar avanço da barra
            processed_any = False
            while True:
                message = self.data_queue.get_nowait()
                processed_any = True
                msg_type = message.get('type')
                
                if msg_type == 'update':
                    self.live_power_data.append(message['power'])
                    self.live_rssi_data.append(message['rssi'])
                    # Quando um ponto é plotado, limpa current_power para forçar usar o último ponto plotado
                    self.current_power = None
                    self.update_plot()
                elif msg_type == 'current_power':
                    # Atualiza current_power apenas se não houver pontos plotados ainda
                    # Se já houver pontos, não atualiza (barra segue os pontos)
                    if not self.live_power_data:
                        self.current_power = message['power']
                        self.update_plot()
                elif msg_type in ['complete', 'stop', 'error']:
                    self.test_running = False
                    
                    # Salva no histórico apenas quando o teste completa normalmente
                    if msg_type == 'complete' and self.live_power_data:
                        self.save_test_to_history()
                    
                    # Preserva dados temporários apenas quando completar
                    if msg_type == 'complete' and self.live_power_data and self.current_test_name:
                        current_test_name = self.test_name_var.get()
                        temp_id = abs(hash(current_test_name)) % 1000
                        self.multiple_tests[current_test_name] = {
                            'data': {
                                'power': self.live_power_data.copy(),
                                'rssi': self.live_rssi_data.copy()
                            },
                            'id': temp_id
                        }
                    
                    self.live_power_data.clear()
                    self.live_rssi_data.clear()
                    self.current_power = None
                    
                    # NOVO: Atualiza apenas o plot atual mantendo a cor original, sem recarregar do histórico
                    self.update_plot()
                    
                    self.update_ui_state()
                    if msg_type == 'complete': 
                        messagebox.showinfo(t('rssi_power.test_completed'), t('rssi_power.test_completed_msg'))
                    elif msg_type == 'error': 
                        messagebox.showerror(t('rssi_power.test_error'), message['error'])
        except queue.Empty:
            pass
        self.after(50, self.process_data_queue)

    def _set_power(self, power_dbm):
        power_val = int(power_dbm * 100)
        power_data = bytes([0, 0]) + power_val.to_bytes(2, 'big') * 2
        out_buf = ctypes.create_string_buffer(32); out_len = ctypes.c_uint(0)
        return rfid_sdk.UHF_RFID_Set(RFID_CMD_SET_TXPOWER, ctypes.c_char_p(power_data), 6, out_buf, ctypes.byref(out_len)) == 0

    def _set_frequency(self, freq_mhz):
        freq_data = (1).to_bytes(1, 'big') + int(freq_mhz * 1000).to_bytes(3, 'big')
        out_buf = ctypes.create_string_buffer(32); out_len = ctypes.c_uint(0)
        return rfid_sdk.UHF_RFID_Set(RFID_CMD_SET_FREQ_TABLE, ctypes.c_char_p(freq_data), 4, out_buf, ctypes.byref(out_len)) == 0

    def _scan_for_tags_internal(self, duration_sec=0.5):
        tags_found = {}
        start_time = time.time()
        while time.time() - start_time < duration_sec:
            out_buf = ctypes.create_string_buffer(256); out_len = ctypes.c_uint(0)
            status = rfid_sdk.UHF_RFID_Set(RFID_CMD_INV_TAG, bytes([0x00, 0x64]), 2, out_buf, ctypes.byref(out_len))
            if status == 0 and out_len.value > 5:
                try:
                    epc = out_buf.raw[2:out_len.value - 3].hex().upper()
                    rssi_bytes = out_buf.raw[out_len.value - 3:out_len.value - 1]
                    rssi = struct.unpack('>h', rssi_bytes)[0] / 10.0
                    if -100 <= rssi <= 0 and (epc not in tags_found or rssi > tags_found[epc].rssi):
                        tags_found[epc] = TagInfo(epc=epc, rssi=rssi)
                except (struct.error, IndexError): continue
            time.sleep(0.01)
        return list(tags_found.values())

    def on_tag_selected(self):
        selected_epc = self.selected_tag_var.get()
        if not selected_epc: return
        self.selected_tag_info = next((tag for tag in self.available_tags if tag.epc == selected_epc), None)
        if self.selected_tag_info:
            # CORREÇÃO: Só habilita o botão se houver licença
            if self.license_limits.get('is_licensed', False):
                self.start_test_button.config(state="normal" if not self.test_running else "disabled")
            else:
                self.start_test_button.config(state="disabled")
            self.selected_tag_label.config(text=f"{t('rssi_power.tag_selected')} ...{selected_epc[-8:]}", fg="blue")
        else:
            self.start_test_button.config(state="disabled")
            self.selected_tag_label.config(text=t('rssi_power.no_tag_selected'), fg="red")
        
        # Atualiza o estado da UI baseado na licença
        self.update_ui_state()

    def show_tags_in_interface(self):
        for widget in self.tags_checkboxes_frame.winfo_children(): widget.destroy()
        self.selected_tag_var = tk.StringVar(value=None)
        for tag in self.available_tags:
            tag_info = f"EPC: ...{tag.epc[-8:]} (RSSI: {tag.rssi:.1f} dBm)"
            rb = ttk.Radiobutton(self.tags_checkboxes_frame, text=tag_info, variable=self.selected_tag_var, value=tag.epc, command=self.on_tag_selected)
            rb.pack(anchor='w')
        self.selected_tag_label.config(text=f"{len(self.available_tags)} tag(s) encontrada(s).")

    def save_test_to_history(self):
        if not self.database: return
        try:
            # Calcula o slope para este teste
            slope_value = self.calculate_linear_slope(self.live_power_data, self.live_rssi_data)
            
            test_data = {
                "test_name": self.test_name_var.get(),
                "tag_epc": self.selected_tag_info.epc,
                "frequency": self.freq_var.get(),
                "power_data": self.live_power_data,
                "rssi_data": self.live_rssi_data,
                "slope": slope_value,  # NOVO: Slope calculado
                "timestamp": datetime.now().strftime("%d-%m-%Y %H:%M"),
                "show_in_graph": True
            }
            if self.database.add_test_to_history(test_data):
                # NOVO: Preserva dados e cor do teste antes de limpar
                if self.live_power_data and self.current_test_name:
                    current_test_name = self.test_name_var.get()
                    temp_id = abs(hash(current_test_name)) % 1000
                    
                    self.multiple_tests[current_test_name] = {
                        'data': {
                            'power': self.live_power_data.copy(),
                            'rssi': self.live_rssi_data.copy()
                        },
                        'id': temp_id
                    }
                
                self.load_history_to_tree()
                self.update_history_stats()
        except Exception as e:
            pass

    def load_history_to_tree(self):
        if not self.database: return
        for item in self.history_tree.get_children(): self.history_tree.delete(item)
        history = self.database.get_test_history()
        
        # Preenche do mais antigo para o mais novo (de cima para baixo)
        for test in history:
            p_data = test.get('power_data', [])
            r_data = test.get('rssi_data', [])
            p_range = f"{min(p_data):.1f} a {max(p_data):.1f}" if p_data else "N/A"
            r_range = f"{min(r_data):.1f} a {max(r_data):.1f}" if r_data else "N/A"
            epc = test.get('tag_epc', 'N/A')
            
            # Calcula o slope para este teste (usa o salvo se disponível, senão recalcula)
            if 'slope' in test and test['slope'] is not None:
                slope_value = test['slope']
            else:
                slope_value = self.calculate_linear_slope(p_data, r_data)
            
            # Formata timestamp conforme idioma
            timestamp_raw = test.get('timestamp', '')
            if isinstance(timestamp_raw, str) and timestamp_raw:
                try:
                    from datetime import datetime
                    if 'T' in timestamp_raw:
                        timestamp_dt = datetime.fromisoformat(timestamp_raw.replace('Z', '+00:00'))
                    else:
                        timestamp_dt = datetime.strptime(timestamp_raw, '%d/%m/%Y %H:%M:%S')
                    from .i18n import get_translator
                    if get_translator().get_language() == 'en':
                        timestamp_formatted = timestamp_dt.strftime('%m/%d/%y %H:%M:%S')
                    else:
                        timestamp_formatted = timestamp_dt.strftime('%d/%m/%Y %H:%M:%S')
                except:
                    timestamp_formatted = timestamp_raw
            else:
                timestamp_formatted = str(timestamp_raw) if timestamp_raw else ''
            
            self.history_tree.insert("", "end", values=(
                "☑" if test.get('show_in_graph') else "☐",
                test.get('id'),
                test.get('test_name'),
                epc,  # NOVO: Mostra EPC completo
                test.get('frequency'),
                p_range,
                r_range,
                f"{slope_value:.3f}",  # NOVO: Slope calculado
                timestamp_formatted
            ))
        # Sempre mostrar o último teste (scroll para a última linha)
        try:
            items = self.history_tree.get_children()
            if items:
                last_item = items[-1]
                self.history_tree.see(last_item)
                self.history_tree.selection_set(last_item)
        except Exception:
            pass
        self.update_history_stats()

    def calculate_linear_slope(self, power_data, rssi_data):
        """
        Calcula a inclinação linear (slope) da curva RSSI vs Potência
        
        Args:
            power_data (list): Lista de valores de potência em dBm
            rssi_data (list): Lista de valores de RSSI em dBm
            
        Returns:
            float: Slope em dBm/dBm (inclinação da linha de tendência)
        """
        try:
            if len(power_data) < 2 or len(rssi_data) < 2:
                return 0.0
            
            # Converte para arrays numpy para cálculos mais eficientes
            import numpy as np
            power_array = np.array(power_data)
            rssi_array = np.array(rssi_data)
            
            # Calcula a inclinação usando regressão linear (método dos mínimos quadrados)
            # Fórmula: slope = Σ((x-x̄)(y-ȳ)) / Σ((x-x̄)²)
            power_mean = np.mean(power_array)
            rssi_mean = np.mean(rssi_array)
            
            numerator = np.sum((power_array - power_mean) * (rssi_array - rssi_mean))
            denominator = np.sum((power_array - power_mean) ** 2)
            
            if denominator == 0:
                return 0.0
            
            slope = numerator / denominator
            
            # Arredonda para 3 casas decimais
            return round(slope, 3)
            
        except Exception as e:
            return 0.0
    
    def update_history_stats(self):
        if not self.database: return
        stats = self.database.get_statistics()
        
        # Calcula estatísticas de slope se houver testes
        slope_stats = ""
        history = self.database.get_test_history()
        if history:
            slopes = []
            for test in history:
                if 'slope' in test and test['slope'] is not None:
                    slopes.append(test['slope'])
                else:
                    # Recalcula slope se não estiver salvo
                    p_data = test.get('power_data', [])
                    r_data = test.get('rssi_data', [])
                    if p_data and r_data:
                        slope = self.calculate_linear_slope(p_data, r_data)
                        slopes.append(slope)
            
            if slopes:
                import numpy as np
                avg_slope = np.mean(slopes)
                min_slope = np.min(slopes)
                max_slope = np.max(slopes)
                slope_stats = t('rssi_power.slope_stats').format(avg=avg_slope, min=min_slope, max=max_slope)
        
        # Traduz o texto de estatísticas
        count = stats['total_tests']
        tags = stats['unique_tags_tested']
        if count == 1:
            stats_text = t('rssi_power.stats_format_single').format(count=count, tags=tags, slope=slope_stats)
        else:
            stats_text = t('rssi_power.stats_format').format(count=count, tags=tags, slope=slope_stats)
        
        self.stats_label.config(text=stats_text)

    def on_history_tree_click(self, event):
        region = self.history_tree.identify("region", event.x, event.y)
        
        # Só aceita cliques na coluna do checkbox (coluna 1)
        if region == "cell":
            column_id = self.history_tree.identify_column(event.x)
            # Coluna 1 é o checkbox (Plot)
            if column_id == '#1':
                item = self.history_tree.identify_row(event.y)
                if not item: return
                test_id = self.history_tree.item(item)['values'][1]
                test_data = self.database.get_test_by_id(test_id)
                if test_data:
                    test_data['show_in_graph'] = not test_data.get('show_in_graph', False)
                    self.database.update_test_in_history(test_id, test_data)
                    self.load_history_to_tree()
                    self.update_plot_from_history()
    
    # --- NOVOS MÉTODOS PARA EDIÇÃO IN-LINE ---
    def on_history_tree_double_click(self, event):
        """Manipula o duplo-clique para edição in-line do nome do teste."""
        if hasattr(self, '_edit_entry') and self._edit_entry.winfo_exists():
            self._edit_entry.destroy()

        region = self.history_tree.identify("region", event.x, event.y)
        if region != "cell": return

        column_id = self.history_tree.identify_column(event.x)
        if column_id != '#2': return
            
        item_id = self.history_tree.identify_row(event.y)
        if not item_id: return

        x, y, width, height = self.history_tree.bbox(item_id, column_id)

        current_name = self.history_tree.set(item_id, 'Nome')
        self._edit_var = tk.StringVar(value=current_name)
        self._edit_entry = ttk.Entry(self.history_tree, textvariable=self._edit_var)
        self._edit_entry.place(x=x, y=y, width=width, height=height)
        self._edit_entry.focus_set()
        self._edit_entry.selection_range(0, 'end')

        self._edit_entry.bind("<Return>", lambda e: self.save_edited_name(item_id))
        self._edit_entry.bind("<FocusOut>", lambda e: self.save_edited_name(item_id))
        self._edit_entry.bind("<Escape>", lambda e: self.cancel_edit_name())

    def save_edited_name(self, item_id):
        """Salva o nome do teste que foi editado."""
        if not (hasattr(self, '_edit_entry') and self._edit_entry.winfo_exists()): return
            
        try:
            new_name = self._edit_var.get().strip()
            self.cancel_edit_name()

            if not new_name: return

            test_id = self.history_tree.item(item_id)['values'][1]

            if self.database:
                # NOVO: Verifica se o novo nome já existe (excluindo o teste atual)
                if self.check_duplicate_test_name_excluding_current(new_name, test_id):
                    messagebox.showerror(t('rssi_power.name_duplicate_inline'), 
                                       t('rssi_power.name_duplicate_inline_msg').format(name=new_name), 
                                       parent=self)
                    return
                
                test_data = self.database.get_test_by_id(test_id)
                if test_data:
                    test_data['test_name'] = new_name
                    if self.database.update_test_in_history(test_id, test_data):
                        self.history_tree.set(item_id, 'Nome', new_name)
                        self.update_plot_from_history()
        except Exception as e:
            pass

    def cancel_edit_name(self, event=None):
        """Cancela o processo de edição e destrói o widget."""
        if hasattr(self, '_edit_entry') and self._edit_entry.winfo_exists():
            self._edit_entry.destroy()
            delattr(self, '_edit_entry')

    # --- FIM DOS MÉTODOS DE EDIÇÃO ---

    def update_plot_from_history(self):
        if not self.database: return
        history = self.database.get_test_history()
        
        # NOVO: Limpa e recarrega dados históricos no sistema multiple_tests
        self.multiple_tests.clear()
        
        for test in history:
            name = test.get('test_name', f'Teste_{test.get("id", "unknown")}')
            temp_id = abs(hash(name)) % 1000
            
            # Só adiciona ao multiple_tests se show_in_graph for True
            if test.get('show_in_graph'):
                self.multiple_tests[name] = {
                    'data': {
                        'power': test.get('power_data', []),
                        'rssi': test.get('rssi_data', [])
                    },
                    'id': temp_id
                }
        
        self.update_plot()

    def clear_plot_and_selection(self):
        # Se estiver executando, interrompe sem salvar
        if getattr(self, 'test_running', False):
            self.test_running = False
        self.live_power_data.clear()
        self.live_rssi_data.clear()
        self.historical_plots.clear()
        self.multiple_tests.clear()  # NOVO: Limpa também o sistema de múltiplos testes
        self.deselect_all_tests()

    def get_selected_test_ids_from_tree(self):
        if not hasattr(self, 'history_tree'): return []
        return [self.history_tree.item(item)['values'][1] for item in self.history_tree.get_children() if self.history_tree.set(item, 'Plot') == "☑"]
    
    def select_all_tests(self):
        if not self.database: return
        for item in self.history_tree.get_children():
            test_id = self.history_tree.item(item)['values'][1]
            test_data = self.database.get_test_by_id(test_id)
            if test_data and not test_data.get('show_in_graph'):
                test_data['show_in_graph'] = True
                self.database.update_test_in_history(test_id, test_data)
        self.load_history_to_tree(); self.update_plot_from_history()

    def deselect_all_tests(self):
        if not self.database: return
        for item in self.history_tree.get_children():
            test_id = self.history_tree.item(item)['values'][1]
            test_data = self.database.get_test_by_id(test_id)
            if test_data and test_data.get('show_in_graph'):
                test_data['show_in_graph'] = False
                self.database.update_test_in_history(test_id, test_data)
        self.load_history_to_tree(); self.update_plot_from_history()

    def save_selected_tests(self):
        if not self.database: return
        selected_ids = self.get_selected_test_ids_from_tree()
        if not selected_ids:
            messagebox.showwarning(t('rssi_power.no_test_selected_save'), t('rssi_power.no_test_selected_save_msg'), parent=self); return
        tests_to_save = [self.database.get_test_by_id(tid) for tid in selected_ids if self.database.get_test_by_id(tid)]
        # Nome sugerido padronizado: Nome do Módulo dd.mm.aa_hh.mm.ss.json
        filepath = filedialog.asksaveasfilename(
            defaultextension=".json",
            filetypes=[("JSON Files", "*.json")],
            title=t('rssi_power.save_tests_title'),
            initialfile=f"RSSI x Power {datetime.now():%d.%m.%y_%H.%M.%S}.json",
            parent=self
        )
        if not filepath: return
        try:
            with open(filepath, "w", encoding='utf-8') as f:
                json.dump({"test_data": tests_to_save}, f, indent=2)
            messagebox.showinfo(t('rssi_power.success'), t('rssi_power.tests_saved_success').format(count=len(tests_to_save)), parent=self)
        except Exception as e: messagebox.showerror(t('rssi_power.error_saving'), t('rssi_power.error_saving_msg').format(error=str(e)), parent=self)

    def import_tests(self):
        if not self.database: return
        filepath = filedialog.askopenfilename(filetypes=[("JSON Files", "*.json")], title=t('rssi_power.import_tests_title'), parent=self)
        if not filepath: return
        try:
            with open(filepath, "r", encoding='utf-8') as f:
                data = json.load(f)
            tests_to_import = data.get("test_data", [])
            if not tests_to_import:
                messagebox.showwarning(t('rssi_power.no_tests_in_file'), t('rssi_power.no_tests_in_file_msg'), parent=self); return
            for test in tests_to_import:
                if 'id' in test: del test['id']
                test['show_in_graph'] = True
                self.database.add_test_to_history(test)
            self.load_history_to_tree(); self.update_plot_from_history()
            messagebox.showinfo(t('rssi_power.import_completed'), t('rssi_power.import_completed_msg').format(count=len(tests_to_import)), parent=self)
        except Exception as e: messagebox.showerror(t('rssi_power.import_error'), t('rssi_power.import_error_msg').format(error=str(e)), parent=self)
    
    def delete_selected_tests(self):
        if not self.database: return
        selected_ids = self.get_selected_test_ids_from_tree()
        if not selected_ids:
            messagebox.showwarning(t('rssi_power.delete_tests_title'), t('rssi_power.delete_tests_msg'), parent=self); return

        for tid in selected_ids: self.database.delete_test_from_history(tid)
        self.load_history_to_tree(); self.update_plot_from_history()
        messagebox.showinfo(t('rssi_power.tests_deleted'), t('rssi_power.tests_deleted_msg').format(count=len(selected_ids)), parent=self)

    def sort_treeview(self, column):
        """
        Ordena a tabela de histórico pela coluna especificada
        
        Args:
            column (str): Nome da coluna para ordenação
            
        Melhorias implementadas:
        - Ordenação específica para cada tipo de coluna
        - Tratamento robusto de valores vazios e inválidos
        - Fallback para ordenação alfabética em caso de erro
        - Performance otimizada para tabelas grandes
        """
        try:
            # Obtém todos os itens da tabela
            items = [(self.history_tree.set(item, column), item) for item in self.history_tree.get_children('')]
            
            # Verifica se há itens para ordenar
            if not items:
                return
            
            # Verifica se é a mesma coluna para alternar direção
            if self.current_sort_column == column:
                self.current_sort_reverse = not self.current_sort_reverse
            else:
                self.current_sort_column = column
                self.current_sort_reverse = False
            
            # Determina o tipo de ordenação baseado na coluna
            if column == "Frequência":
                # Ordenação numérica decimal para frequência
                def freq_key(x):
                    try:
                        if x[0] == "" or x[0] == "N/A":
                            return 0
                        # Remove " MHz" e converte para float
                        freq_val = float(x[0].replace(" MHz", ""))
                        # Para frequência: ordena do menor para o maior (banda baixa para alta)
                        return freq_val
                    except (ValueError, AttributeError):
                        return 0
                # Para Frequência: ordenação crescente (banda baixa para alta)
                items.sort(key=freq_key, reverse=self.current_sort_reverse)
            elif column == "Slope":
                # Ordenação específica para Slope (inclinação linear)
                def slope_key(x):
                    try:
                        if x[0] == "" or x[0] == "N/A" or x[0] == "0.000":
                            return 0.0
                        # Converte para float para ordenação numérica
                        slope_val = float(x[0])
                        # Para Slope: ordena por valor absoluto (mais sensível primeiro)
                        # Valores positivos maiores indicam maior sensibilidade
                        return abs(slope_val)
                    except (ValueError, AttributeError):
                        return 0.0
                # Para Slope: ordenação por sensibilidade - maior slope primeiro (mais sensível)
                items.sort(key=slope_key, reverse=self.current_sort_reverse)
            elif column == "Range RSSI":
                # Ordenação específica para RSSI (valores negativos)
                def rssi_range_key(x):
                    try:
                        if x[0] == "" or x[0] == "N/A":
                            return 0
                        # Remove " dBm" e extrai os valores
                        clean_value = x[0].replace(" dBm", "")
                        if " a " in clean_value:
                            # Formato "15.0 a 25.0" - para RSSI usa o valor médio para ordenação mais intuitiva
                            parts = clean_value.split(" a ")
                            if len(parts) == 2:
                                min_val = float(parts[0])
                                max_val = float(parts[1])
                                # Para RSSI: ordena por valor médio (mais representativo)
                                # Como RSSI é negativo, valores mais próximos de zero aparecem primeiro
                                # Também considera a amplitude do range (menor amplitude = mais estável)
                                range_amplitude = abs(max_val - min_val)
                                mean_value = (min_val + max_val) / 2
                                # Combina valor médio com estabilidade (menor amplitude = melhor)
                                return mean_value + (range_amplitude * 0.1)  # Peso menor para amplitude
                        else:
                            # Valor único
                            return float(clean_value)
                    except (ValueError, AttributeError):
                        return 0
                # Para RSSI: ordenação especial - valores mais próximos de zero primeiro (melhor sinal)
                # Como RSSI é negativo, ordenamos do menos negativo (melhor) para o mais negativo (pior)
                # Também considera estabilidade do sinal
                items.sort(key=rssi_range_key, reverse=self.current_sort_reverse)
            elif column == "Range Potência":
                # Ordenação específica para Potência (valores positivos)
                def power_range_key(x):
                    try:
                        if x[0] == "" or x[0] == "N/A":
                            return 0
                        # Remove " dBm" e extrai os valores
                        clean_value = x[0].replace(" dBm", "")
                        if " a " in clean_value:
                            # Formato "15.0 a 25.0" - para Potência usa o valor mínimo
                            parts = clean_value.split(" a ")
                            if len(parts) == 2:
                                min_val = float(parts[0])
                                max_val = float(parts[1])
                                # Para Potência: ordena por potência mínima (mais eficiente)
                                # Também considera a amplitude do range (menor amplitude = mais estável)
                                range_amplitude = abs(max_val - min_val)
                                # Combina potência mínima com estabilidade
                                return min_val + (range_amplitude * 0.05)  # Peso menor para amplitude
                        else:
                            # Valor único
                            return float(clean_value)
                    except (ValueError, AttributeError):
                        return 0
                # Para Potência: ordenação por eficiência - menor potência primeiro (mais eficiente)
                # Também considera estabilidade do sinal
                items.sort(key=power_range_key, reverse=self.current_sort_reverse)
            elif column == "Data/Hora":
                # Ordenação de data/hora (formato brasileiro: DD/MM/AAAA HH:MM)
                def datetime_key(x):
                    try:
                        if x[0] == "" or x[0] == "N/A":
                            return ""
                        
                        # Converte data/hora para objeto datetime para ordenação correta
                        # Formato usado: "DD-MM-AAAA HH:MM" (brasileiro)
                        return datetime.strptime(x[0], "%d-%m-%Y %H:%M")
                        
                    except (ValueError, AttributeError) as e:
                        # Tenta formato alternativo se o principal falhar
                        try:
                            return datetime.strptime(x[0], "%Y-%m-%d %H:%M:%S")
                        except (ValueError, AttributeError):
                            # Tenta formato com segundos se o anterior falhar
                            try:
                                return datetime.strptime(x[0], "%d/%m/%Y %H:%M:%S")
                            except (ValueError, AttributeError):
                                # Fallback para ordenação alfabética em caso de formato não reconhecido
                                return x[0]
                
                # Para Data/Hora: ordenação cronológica (mais recente primeiro ou último)
                items.sort(key=datetime_key, reverse=self.current_sort_reverse)
            elif column == "Plot":
                # Ordenação por estado (☐ primeiro, depois ☑)
                def plot_key(x):
                    if x[0] == "☐":
                        return 0  # Não plotado
                    elif x[0] == "☑":
                        return 1  # Plotado
                    else:
                        return 2  # Outros estados
                # Para Plot: ordenação lógica (não plotado, plotado, outros)
                items.sort(key=plot_key, reverse=self.current_sort_reverse)
            else:
                # Ordenação alfabética para outras colunas (Nome, EPC da Tag)
                def alpha_key(x):
                    if x[0] == "" or x[0] == "N/A":
                        return ""  # Valores vazios ficam por último
                    return x[0].lower()
                # Para outras colunas: ordenação alfabética com valores vazios por último
                items.sort(key=alpha_key, reverse=self.current_sort_reverse)
            
            # Reorganiza os itens na tabela
            for index, (val, item) in enumerate(items):
                self.history_tree.move(item, '', index)
            
            # Atualiza o símbolo de ordenação no cabeçalho
            self.update_sort_indicator(column)
            
            # Feedback visual da ordenação
            direction = "decrescente" if self.current_sort_reverse else "crescente"
            
        except Exception as e:
            # Em caso de erro, tenta ordenação alfabética simples como fallback
            try:
                items.sort(key=lambda x: str(x[0]).lower(), reverse=self.current_sort_reverse)
            except Exception as fallback_error:
                pass
    
    def update_sort_indicator(self, column):
        """
        Atualiza os símbolos de ordenação nos cabeçalhos
        """
        try:
            # Remove símbolos de todas as colunas
            for col in ["Plot", "Nome", "EPC da Tag", "Frequência", "Range Potência", "Range RSSI", "Slope", "Data/Hora"]:
                current_text = self.history_tree.heading(col)['text']
                # Remove símbolos existentes
                if " ↑" in current_text:
                    current_text = current_text.replace(" ↑", "")
                elif " ↓" in current_text:
                    current_text = current_text.replace(" ↓", "")
                # Adiciona símbolo neutro
                if not current_text.endswith(" ↕"):
                    current_text += " ↕"
                self.history_tree.heading(col, text=current_text)
            
            # Adiciona símbolo de direção na coluna atual
            current_text = self.history_tree.heading(column)['text']
            current_text = current_text.replace(" ↕", "")
            if self.current_sort_reverse:
                current_text += " ↓"
            else:
                current_text += " ↑"
            self.history_tree.heading(column, text=current_text)
            
        except Exception as e:
            # Em caso de erro, tenta restaurar estado neutro
            try:
                for col in ["Plot", "Nome", "EPC da Tag", "Frequência", "Range Potência", "Range RSSI", "Data/Hora"]:
                    current_text = self.history_tree.heading(col)['text']
                    # Remove todos os símbolos e adiciona neutro
                    current_text = current_text.replace(" ↑", "").replace(" ↓", "").replace(" ↕", "")
                    current_text += " ↕"
                    self.history_tree.heading(col, text=current_text)
            except Exception as restore_error:
                pass

    def generate_pdf_report(self):
        """Gera relatório PDF com diálogo para salvar arquivo - apenas testes selecionados"""
        try:
            # Verifica se há dados para relatório
            if not DATABASE_AVAILABLE or not self.database:
                messagebox.showwarning(t('rssi_power.db_unavailable_report'), t('rssi_power.db_unavailable_report_msg'), parent=self)
                return
            
            # Obtém apenas os testes selecionados
            selected_ids = self.get_selected_test_ids_from_tree()
            if not selected_ids:
                messagebox.showwarning(t('rssi_power.no_test_selected_report'), t('rssi_power.no_test_selected_report_msg'), parent=self)
                return
            
            # Carrega apenas os testes selecionados
            history_data = []
            print(f"🔍 DEBUG: IDs selecionados: {selected_ids}")
            for test_id in selected_ids:
                test_data = self.database.get_test_by_id(test_id)
                if test_data:
                    print(f"   ✅ Teste ID {test_id} carregado: {test_data.get('test_name', 'N/A')}")
                    history_data.append(test_data)
                else:
                    print(f"   ⚠️ Teste ID {test_id} não encontrado no banco de dados")
            
            print(f"🔍 DEBUG: Total de testes carregados para o PDF: {len(history_data)}")
            if not history_data:
                messagebox.showwarning(t('rssi_power.no_valid_test'), t('rssi_power.no_valid_test_msg'), parent=self)
                return
            
            # Gera nome do arquivo com data e hora
            now = datetime.now()
            filename = f"RSSI Power Reporte Selecionados {now.strftime('%d.%m.%Y_%H.%M.%S')}.pdf"
            
            # Diálogo para salvar arquivo
            filepath = filedialog.asksaveasfilename(
                defaultextension=".pdf",
                filetypes=[("PDF Files", "*.pdf")],
                initialfile=filename,
                title=t('rssi_power.save_pdf_title'),
                parent=self
            )
            
            if not filepath:
                return  # Usuário cancelou
            
            # Mostra mensagem de progresso
            print("Gerando relatório PDF...")
            self.update()
            
            # Gera o PDF com dados selecionados
            result = self._generate_pdf_with_selected_tests(filepath, history_data)
            
            if result['success']:
                print("Relatório PDF dos testes selecionados gerado com sucesso!")
                messagebox.showinfo(t('rssi_power.success'), t('rssi_power.success_report_generated').format(filepath=filepath, count=len(history_data)), parent=self)
                
                # Abre o PDF automaticamente
                try:
                    os.startfile(filepath)
                except Exception:
                    pass  # Ignora erro se não conseguir abrir
            else:
                error_msg = result.get('error', 'Erro desconhecido')
                print(f"Erro ao gerar relatório PDF: {error_msg}")
                messagebox.showerror(t('rssi_power.error_generating_pdf'), t('rssi_power.error_generating_pdf_msg').format(error=error_msg), parent=self)
                
        except Exception as e:
            print(f"Erro ao gerar relatório PDF: {str(e)}")
            import traceback
            traceback.print_exc()
            messagebox.showerror(t('rssi_power.error_generating_pdf'), t('rssi_power.error_unexpected_pdf').format(error=str(e)), parent=self)

    def _generate_pdf_with_selected_tests(self, filepath, selected_tests):
        """Gera PDF usando ReportLab com dados selecionados - mesmo método dos outros módulos"""
        try:
            print(f"Gerando PDF para {len(selected_tests)} testes...")
            print(f"Caminho do arquivo: {filepath}")
            print(f"🔍 DEBUG: Testes recebidos para PDF:")
            for i, test in enumerate(selected_tests):
                print(f"   Teste {i+1}: {test.get('test_name', 'N/A')} - EPC: {test.get('tag_epc', 'N/A')}")
            
            from reportlab.lib.pagesizes import A4
            from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
            from reportlab.lib.units import inch
            from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Image, PageBreak, KeepTogether
            from reportlab.lib import colors
            from reportlab.lib.enums import TA_CENTER, TA_LEFT
            # Função para obter informações do sistema da licença ativa
            def _get_system_info():
                """Obtém informações do sistema da licença ativa"""
                try:
                    # Tenta obter informações via app_shell (método preferido)
                    if hasattr(self, 'app_shell') and self.app_shell and hasattr(self.app_shell, 'license_manager'):
                        system_info = self.app_shell.license_manager.get_active_license_system_info(self.com_port)
                        # Mantém a versão do software (4.0.0) sem sobrescrever com nome do módulo
                        return system_info
                    
                    # Fallback: cria LicenseManager temporário
                    try:
                        from .license_module import LicenseManager
                        license_manager = LicenseManager(LICENSE_DB_FILE)
                        system_info = license_manager.get_active_license_system_info(self.com_port)
                        # Mantém a versão do software (4.0.0) sem sobrescrever com nome do módulo
                        return system_info
                    except Exception as fallback_error:
                        print(f"⚠️ Erro no fallback de informações do sistema: {fallback_error}")
                    
                    # Fallback final: informações básicas
                    from datetime import datetime
                    from .i18n import get_translator
                    translator = get_translator()
                    date_format = '%d/%m/%Y %H:%M:%S' if translator.get_language() == 'pt' else '%m/%d/%Y %I:%M:%S %p'
                    return {
                        'software': '4.0.0',
                        'hardware': 'N/A',
                        'firmware': 'N/A',
                        'serial_number': 'N/A',
                        'license': 'N/A',
                        'generated_at': datetime.now().strftime(date_format)
                    }
                    
                except Exception as e:
                    print(f"⚠️ Erro geral ao obter informações do sistema: {e}")
                    from datetime import datetime
                    from .i18n import get_translator
                    translator = get_translator()
                    date_format = '%d/%m/%Y %H:%M:%S' if translator.get_language() == 'pt' else '%m/%d/%Y %I:%M:%S %p'
                    return {
                        'software': '4.0.0',
                        'hardware': 'N/A',
                        'firmware': 'N/A',
                        'serial_number': 'N/A',
                        'license': 'N/A',
                        'generated_at': datetime.now().strftime(date_format)
                    }
            import tempfile
            import base64
            
            print("Importações do ReportLab concluídas com sucesso")
            
            # Cria o documento PDF
            doc = SimpleDocTemplate(filepath, pagesize=A4, 
                                  rightMargin=72, leftMargin=72, 
                                  topMargin=72, bottomMargin=18)
            
            # Estilos
            styles = getSampleStyleSheet()
            title_style = ParagraphStyle(
                'CustomTitle',
                parent=styles['Heading1'],
                fontSize=24,
                spaceAfter=30,
                alignment=TA_CENTER,
                textColor=colors.HexColor('#2c3e50')
            )
            
            heading_style = ParagraphStyle(
                'CustomHeading',
                parent=styles['Heading2'],
                fontSize=16,
                spaceAfter=12,
                textColor=colors.HexColor('#2c3e50')
            )
            
            # Conteúdo do documento
            story = []
            
            # Logo Fasttag - mesmo método do Noise Check
            try:
                root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
                logo_path = os.path.join(root, 'assets', 'images', 'fasttag_logo.png')
                if os.path.exists(logo_path):
                    # Mantém proporção circular (1:1) - logo redondo
                    logo_size = 1.5*inch
                    logo_img = Image(logo_path, width=logo_size, height=logo_size)
                    story.append(logo_img)
                    story.append(Spacer(1, 10))
                    print("Logo Fasttag adicionado ao PDF (proporção circular)")
                else:
                    print("Logo Fasttag não encontrado")
            except Exception as e:
                print(f"Erro ao adicionar logo: {e}")
            
            # Título
            story.append(Paragraph(t('pdf.report_title_rssi'), title_style))
            story.append(Spacer(1, 20))
            
            # Informações do sistema - usa a mesma função dos outros módulos
            sysinfo = _get_system_info()
            story.append(Paragraph(t('pdf.system_info'), heading_style))
            
            info_text = f"""
            <b>{t('pdf.software')}</b> {sysinfo['software']}<br/>
            <b>{t('pdf.hardware')}</b> {sysinfo['hardware']}<br/>
            <b>{t('pdf.firmware')}</b> {sysinfo['firmware']}<br/>
            <b>{t('pdf.serial_number')}</b> {sysinfo['serial_number']}<br/>
            <b>{t('pdf.license')}</b> {sysinfo['license']}<br/>
            <b>{t('pdf.generated_at')}</b> {sysinfo['generated_at']}
            """
            story.append(Paragraph(info_text, styles['Normal']))
            story.append(Spacer(1, 30))
            
            # Descrição do teste
            story.append(Paragraph(t('pdf.test_description_title_rssi'), heading_style))
            test_description = t('pdf.test_description_text_rssi')
            story.append(Paragraph(test_description, styles['Normal']))
            story.append(Spacer(1, 20))
            
            # Tabela do histórico de testes
            story.append(Paragraph(t('pdf.test_history'), heading_style))
            
            table_data = [[t('pdf.name_col'), t('pdf.tag_epc_col'), t('pdf.freq_mhz_col'), t('pdf.power_range'), t('pdf.rssi_range'), t('pdf.slope'), t('pdf.date_time_col')]]
            
            print(f"🔍 DEBUG: Adicionando {len(selected_tests)} testes à tabela de histórico...")
            for idx, test in enumerate(selected_tests):
                print(f"   Processando teste {idx+1}/{len(selected_tests)}: {test.get('test_name', 'N/A')}")
                print(f"      - Chaves disponíveis: {list(test.keys())[:10]}")  # Mostra primeiras 10 chaves
                p_data = test.get('power_data', [])
                r_data = test.get('rssi_data', [])
                print(f"      - power_data: {len(p_data) if isinstance(p_data, list) else type(p_data)} valores")
                print(f"      - rssi_data: {len(r_data) if isinstance(r_data, list) else type(r_data)} valores")
                p_range = f"{min(p_data):.1f} a {max(p_data):.1f}" if p_data else "N/A"
                r_range = f"{min(r_data):.1f} a {max(r_data):.1f}" if r_data else "N/A"
                slope = test.get('slope', 0)
                print(f"      - slope: {slope}, p_range: {p_range}, r_range: {r_range}")
                
                # Formata timestamp conforme idioma - tenta vários formatos como no Antenna Check
                timestamp_raw = test.get('timestamp', 'N/A')
                if timestamp_raw != 'N/A' and isinstance(timestamp_raw, str) and timestamp_raw:
                    try:
                        from datetime import datetime
                        from .i18n import get_translator
                        timestamp_dt = None
                        
                        # Tenta diferentes formatos de data
                        if 'T' in timestamp_raw:
                            # Formato ISO
                            timestamp_dt = datetime.fromisoformat(timestamp_raw.replace('Z', '+00:00'))
                        else:
                            # Lista de formatos para tentar (igual ao Antenna Check)
                            formats_to_try = [
                                '%d/%m/%Y %H:%M:%S',      # Formato com barras e segundos
                                '%d/%m/%Y %H:%M',         # Formato com barras sem segundos
                                '%d-%m-%Y %H:%M:%S',      # Formato com hífens e segundos
                                '%d-%m-%Y %H:%M',         # Formato com hífens sem segundos
                                '%Y-%m-%d %H:%M:%S',      # Formato ISO sem T
                                '%Y-%m-%d %H:%M'          # Formato ISO sem T e sem segundos
                            ]
                            
                            for fmt in formats_to_try:
                                try:
                                    timestamp_dt = datetime.strptime(timestamp_raw, fmt)
                                    break
                                except ValueError:
                                    continue
                        
                        if timestamp_dt:
                            translator = get_translator()
                            if translator.get_language() == 'en':
                                # Se a data original tinha segundos, inclui segundos na saída
                                if ':%S' in timestamp_raw or ':' in timestamp_raw and timestamp_raw.count(':') == 2:
                                    timestamp_formatted = timestamp_dt.strftime('%m/%d/%y %I:%M:%S %p')
                                else:
                                    timestamp_formatted = timestamp_dt.strftime('%m/%d/%y %I:%M %p')
                            else:
                                # Português: mantém o formato original se não tinha segundos
                                if ':%S' in timestamp_raw or ':' in timestamp_raw and timestamp_raw.count(':') == 2:
                                    timestamp_formatted = timestamp_dt.strftime('%d/%m/%Y %H:%M:%S')
                                else:
                                    timestamp_formatted = timestamp_dt.strftime('%d/%m/%Y %H:%M')
                        else:
                            timestamp_formatted = timestamp_raw
                    except:
                        timestamp_formatted = timestamp_raw
                else:
                    timestamp_formatted = timestamp_raw
                
                table_data.append([
                    test.get('test_name', 'N/A'),
                    test.get('tag_epc', 'N/A'),  # EPC completo sem truncagem
                    str(test.get('frequency', 'N/A')),
                    p_range,
                    r_range,
                    f"{slope:.3f}",
                    timestamp_formatted
                ])
                print(f"      ✅ Teste {idx+1} adicionado à tabela: {test.get('test_name', 'N/A')}")
            
            print(f"🔍 DEBUG: Total de linhas na tabela (incluindo cabeçalho): {len(table_data)}")
            
            # Ajusta larguras das colunas para mostrar EPC completo
            history_table = Table(table_data, colWidths=[1.3*inch, 2.2*inch, 0.7*inch, 1*inch, 1*inch, 0.7*inch, 1.1*inch])
            history_table.setStyle(TableStyle([
                ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#007bff')),
                ('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
                ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
                ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
                ('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
                ('FONTSIZE', (0, 0), (-1, -1), 7),  # Reduzido de 8 para 7
                ('FONTSIZE', (0, 1), (1, -1), 6),   # EPC com fonte ainda menor
                ('BOTTOMPADDING', (0, 0), (-1, -1), 6),
                ('TOPPADDING', (0, 0), (-1, -1), 6),
                ('GRID', (0, 0), (-1, -1), 1, colors.black),
                ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#f8f9fa')])
            ]))
            
            # Mantém a tabela inteira na mesma página
            story.append(KeepTogether(history_table))
            story.append(Spacer(1, 30))
            
            # Gráficos - com informações detalhadas como no Antenna Check
            charts = self._generate_rssi_charts_base64(selected_tests)
            temp_files = []  # Lista para armazenar arquivos temporários
            
            for i, (name, image_base64) in enumerate(charts):
                # Encontra o teste correspondente para obter informações adicionais
                test_info = selected_tests[i] if i < len(selected_tests) else {}
                test_id = test_info.get('id', '-')
                timestamp = test_info.get('timestamp', '-')
                
                # Importa get_translator no escopo local
                from .i18n import get_translator
                
                # Formata timestamp conforme idioma - tenta vários formatos como no Antenna Check
                if timestamp != '-' and isinstance(timestamp, str) and timestamp:
                    try:
                        from datetime import datetime
                        translator = get_translator()
                        timestamp_dt = None
                        
                        # Tenta diferentes formatos de data
                        if 'T' in timestamp:
                            # Formato ISO
                            timestamp_dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
                        else:
                            # Lista de formatos para tentar (igual ao Antenna Check)
                            formats_to_try = [
                                '%d/%m/%Y %H:%M:%S',      # Formato com barras e segundos
                                '%d/%m/%Y %H:%M',         # Formato com barras sem segundos
                                '%d-%m-%Y %H:%M:%S',      # Formato com hífens e segundos
                                '%d-%m-%Y %H:%M',         # Formato com hífens sem segundos
                                '%Y-%m-%d %H:%M:%S',      # Formato ISO sem T
                                '%Y-%m-%d %H:%M'          # Formato ISO sem T e sem segundos
                            ]
                            
                            for fmt in formats_to_try:
                                try:
                                    timestamp_dt = datetime.strptime(timestamp, fmt)
                                    break
                                except ValueError:
                                    continue
                        
                        if timestamp_dt:
                            if translator.get_language() == 'en':
                                # Se a data original tinha segundos, inclui segundos na saída
                                if ':%S' in timestamp or ':' in timestamp and timestamp.count(':') == 2:
                                    timestamp_formatted = timestamp_dt.strftime('%m/%d/%y %I:%M:%S %p')
                                else:
                                    timestamp_formatted = timestamp_dt.strftime('%m/%d/%y %I:%M %p')
                            else:
                                # Português: mantém o formato original se não tinha segundos
                                if ':%S' in timestamp or ':' in timestamp and timestamp.count(':') == 2:
                                    timestamp_formatted = timestamp_dt.strftime('%d/%m/%Y %H:%M:%S')
                                else:
                                    timestamp_formatted = timestamp_dt.strftime('%d/%m/%Y %H:%M')
                        else:
                            timestamp_formatted = timestamp
                    except:
                        timestamp_formatted = timestamp
                else:
                    timestamp_formatted = timestamp
                
                # Calcula estatísticas do teste
                power_data = test_info.get('power_data', [])
                rssi_data = test_info.get('rssi_data', [])
                frequency = test_info.get('frequency', 'N/A')
                tag_epc = test_info.get('tag_epc', 'N/A')
                slope = test_info.get('slope', 0)
                
                power_min = min(power_data) if power_data else 0
                power_max = max(power_data) if power_data else 0
                rssi_min = min(rssi_data) if rssi_data else 0
                rssi_max = max(rssi_data) if rssi_data else 0
                rssi_avg = sum(rssi_data) / len(rssi_data) if rssi_data else 0
                num_points = len(power_data) if power_data else 0
                
                # Salva a imagem temporariamente
                with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp_file:
                    tmp_file.write(base64.b64decode(image_base64))
                    tmp_path = tmp_file.name
                    temp_files.append(tmp_path)  # Adiciona à lista para limpeza posterior
                
                # Adiciona a imagem ao PDF
                img = Image(tmp_path, width=7*inch, height=2.5*inch)
                
                # Informações do teste para o gráfico (estilo Antenna Check)
                # Obtém tradução para "Médio/Avg"
                translator = get_translator()
                avg_label = 'Médio' if translator.get_language() == 'pt' else 'Avg'
                power_label = 'Potência' if translator.get_language() == 'pt' else 'Power'
                rssi_label = 'RSSI'
                
                test_info_text = f"""
                <b>{t('pdf.test_id')}</b> {test_id} | <b>{t('pdf.date_time')}</b> {timestamp_formatted} | <b>{t('pdf.tag_epc')}</b> {tag_epc}<br/>
                <b>{t('pdf.frequency')}</b> {frequency} MHz | <b>{t('pdf.points')}</b> {num_points} | <b>{power_label}:</b> {power_min:.1f} - {power_max:.1f} dBm<br/>
                <b>{rssi_label}:</b> {rssi_min:.1f} - {rssi_max:.1f} dBm | <b>{rssi_label} {avg_label}:</b> {rssi_avg:.1f} dBm | <b>{t('pdf.slope')}</b> {slope:.3f} dBm/dBm
                """
                
                # Mantém título, informações, imagem e espaçamento juntos na mesma página
                chart_elements = [
                    Paragraph(f"{t('pdf.rssi_chart_title')} - {name}", heading_style),
                    Paragraph(test_info_text, styles['Normal']),
                    img,
                    Spacer(1, 20)
                ]
                story.append(KeepTogether(chart_elements))
            
            # Rodapé
            story.append(Spacer(1, 30))
            story.append(Paragraph(t('pdf.auto_report_footer'), 
                                 ParagraphStyle('Footer', parent=styles['Normal'], 
                                               fontSize=10, alignment=TA_CENTER, 
                                               textColor=colors.grey)))
            
            # Rodapé do relatório
            story.append(PageBreak())
            story.append(Spacer(1, 30))
            story.append(Paragraph(t('pdf.additional_info'), heading_style))
            
            footer_text = t('pdf.footer_text')
            story.append(Paragraph(footer_text, styles['Normal']))
            
            from datetime import datetime
            # Formata data/hora conforme idioma
            from .i18n import get_translator
            translator = get_translator()
            current_lang = translator.get_language()
            if current_lang == 'en':
                timestamp = datetime.now().strftime("%m/%d/%y at %I:%M:%S %p")
            else:
                timestamp = datetime.now().strftime("%d/%m/%Y às %H:%M:%S")
            story.append(Paragraph(f"<b>{t('pdf.document_generated_at')}</b> {timestamp}", styles['Normal']))
            
            # Gera o PDF
            doc.build(story)
            
            # Limpa arquivos temporários após gerar o PDF - EXATAMENTE igual ao Noise Check
            for temp_file in temp_files:
                try:
                    if os.path.exists(temp_file):
                        os.unlink(temp_file)
                except Exception as e:
                    print(f"⚠️ Erro ao limpar arquivo temporário {temp_file}: {e}")
            
            print("PDF gerado com sucesso!")
            return {'success': True}
            
        except Exception as e:
            print(f"❌ Erro ao gerar PDF: {e}")
            import traceback
            traceback.print_exc()
            return {'success': False, 'error': str(e)}

    def _generate_rssi_charts_base64(self, selected_tests):
        """Gera gráficos RSSI vs Potência em formato base64 - mesmo estilo do Noise Check"""
        charts = []
        
        try:
            import matplotlib
            matplotlib.use('Agg')  # Backend sem GUI
            import matplotlib.pyplot as plt
            import numpy as np
            import io
            import base64
            
            for test in selected_tests:
                try:
                    power_data = test.get('power_data', [])
                    rssi_data = test.get('rssi_data', [])
                    
                    if not power_data or not rssi_data:
                        # Gráfico vazio
                        plt.figure(figsize=(12, 4))  # Mesmo tamanho do Noise Check
                        plt.text(0.5, 0.5, t('rssi_power.graph_no_data'), 
                               ha='center', va='center', transform=plt.gca().transAxes,
                               fontsize=14, color='red')
                    else:
                        # Configuração da figura - formato fixo que cabe no PDF (mesmo do Noise Check)
                        plt.figure(figsize=(12, 4))  # Dimensões fixas que cabem no PDF
                        plt.style.use('default')
                        
                        # Plota os dados com estilo igual ao Noise Check
                        plt.plot(power_data, rssi_data, 'o-', color='#8B4513', linewidth=2, 
                                markersize=4, markerfacecolor='#8B4513', 
                                markeredgecolor='white', markeredgewidth=1)
                        
                        # Calcula e plota linha de tendência (mantém)
                        if len(power_data) >= 2 and len(rssi_data) >= 2:
                            z = np.polyfit(power_data, rssi_data, 1)
                            p = np.poly1d(z)
                            plt.plot(power_data, p(power_data), "r--", alpha=0.8, linewidth=1.2)
                        
                        # Configurações dos eixos (mesmo estilo do Noise Check)
                        plt.xlabel(t('rssi_power.graph_power_label'), fontsize=12)
                        plt.ylabel(t('rssi_power.graph_rssi_label'), fontsize=12)
                        
                        # Grade tracejada igual ao Noise Check
                        plt.grid(True, alpha=0.3, linestyle='--')
                        
                        # Limites dos eixos (ajusta automaticamente aos dados)
                        if power_data:
                            plt.xlim(min(power_data) - 1, max(power_data) + 1)
                        if rssi_data:
                            plt.ylim(min(rssi_data) - 5, max(rssi_data) + 5)
                    
                    # Configurações de layout - garante que tudo seja capturado (mesmo do Noise Check)
                    plt.tight_layout(pad=1.5)
                    
                    # Salva como string base64 - mesmo método do Noise Check
                    buffer = io.BytesIO()
                    plt.savefig(buffer, format='png', dpi=150, facecolor='white', 
                              edgecolor='none', pad_inches=0.2)
                    buffer.seek(0)
                    
                    # Converte para base64
                    image_base64 = base64.b64encode(buffer.getvalue()).decode()
                    buffer.close()
                    plt.close()
                    
                    charts.append((test.get('test_name', 'N/A'), image_base64))
                    
                except Exception as e:
                    print(f"❌ Erro ao gerar gráfico para {test.get('test_name', 'N/A')}: {e}")
                    # Gráfico de erro
                    plt.figure(figsize=(12, 4))
                    plt.text(0.5, 0.5, f"{t('rssi_power.graph_error')} {e}", 
                           ha='center', va='center', transform=plt.gca().transAxes,
                           fontsize=12, color='red')
                    
                    buffer = io.BytesIO()
                    plt.savefig(buffer, format='png', dpi=150, facecolor='white', 
                              edgecolor='none', pad_inches=0.2)
                    buffer.seek(0)
                    image_base64 = base64.b64encode(buffer.getvalue()).decode()
                    buffer.close()
                    plt.close()
                    
                    charts.append((test.get('test_name', 'N/A'), image_base64))
            
        except Exception as e:
            print(f"❌ Erro geral na geração de gráficos: {e}")
        
        return charts

if __name__ == '__main__':
    root = tk.Tk()
    root.title("Teste RSSI x Power")
    root.geometry("1100x800")
    
    from port_manager import get_com_port_number
    com_port = get_com_port_number()
    if com_port is None:
        # CORREÇÃO: Não exibe erro de hardware quando sem licença (modo browser)
        print("ℹ️ Modo browser - hardware não encontrado")
        # Cria uma instância com porta padrão para modo browser
        app = RSSIPowerModule(root, com_port=4)  # COM4 como padrão
        app.pack(fill="both", expand=True)
        root.mainloop()
    else:
        app = RSSIPowerModule(root, com_port=com_port)
        app.pack(fill="both", expand=True)
        root.mainloop()